Repository: trufflesecurity/trufflehog Branch: main Commit: afd5336caad0 Files: 3357 Total size: 15.6 MB Directory structure: gitextract_ieqdewfm/ ├── .captain/ │ └── config.yaml ├── .gitattributes ├── .github/ │ ├── ISSUE_TEMPLATE/ │ │ ├── bug_report.md │ │ └── feature_request.md │ ├── PULL_REQUEST_TEMPLATE.md │ ├── community_note.md │ ├── renovate.json │ └── workflows/ │ ├── README.md │ ├── TESTING.md │ ├── codeql-analysis.yml │ ├── detector-tests.yml │ ├── lint.yml │ ├── performance.yml │ ├── release-guard.yml │ ├── release.yml │ ├── secrets.yml │ ├── smoke.yml │ └── test.yml ├── .gitignore ├── .goreleaser.yml ├── .pre-commit-config.yaml ├── .pre-commit-hooks.yaml ├── CODEOWNERS ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Dockerfile ├── Dockerfile.goreleaser ├── LICENSE ├── Makefile ├── PreCommit.md ├── README.md ├── SECURITY.md ├── action.yml ├── docs/ │ ├── concurrency.md │ ├── iterative_decoding_performance.md │ └── process_flow.md ├── entrypoint.sh ├── examples/ │ ├── README.md │ ├── generic.yml │ └── generic_with_filters.yml ├── go.mod ├── go.sum ├── hack/ │ ├── Dockerfile.protos │ ├── bench/ │ │ ├── plot.gp │ │ ├── plot.sh │ │ ├── plot.txt │ │ └── versions.sh │ ├── docs/ │ │ ├── Adding_Detectors_Internal.md │ │ └── Adding_Detectors_external.md │ ├── generate/ │ │ ├── generate.go │ │ └── test.sh │ ├── semgrep-rules/ │ │ └── detectors.yaml │ └── snifftest/ │ ├── README.md │ ├── main.go │ └── snifftest.sh ├── main.go ├── pkg/ │ ├── analyzer/ │ │ ├── README.md │ │ ├── analyzers/ │ │ │ ├── airbrake/ │ │ │ │ ├── airbrake.go │ │ │ │ └── scopes.go │ │ │ ├── airtable/ │ │ │ │ ├── airtableoauth/ │ │ │ │ │ ├── airtable.go │ │ │ │ │ ├── airtable_test.go │ │ │ │ │ └── expected_output.json │ │ │ │ ├── airtablepat/ │ │ │ │ │ ├── airtable.go │ │ │ │ │ ├── airtable_test.go │ │ │ │ │ ├── expected_output.json │ │ │ │ │ └── requests.go │ │ │ │ └── common/ │ │ │ │ ├── common.go │ │ │ │ ├── endpoints.go │ │ │ │ ├── models.go │ │ │ │ ├── permissions.go │ │ │ │ ├── permissions.yaml │ │ │ │ └── scopes.go │ │ │ ├── analyzers.go │ │ │ ├── anthropic/ │ │ │ │ ├── anthropic.go │ │ │ │ ├── anthropic_test.go │ │ │ │ ├── permissions.go │ │ │ │ ├── permissions.yaml │ │ │ │ ├── requests.go │ │ │ │ └── result_output.json │ │ │ ├── asana/ │ │ │ │ ├── asana.go │ │ │ │ ├── asana_test.go │ │ │ │ ├── expected_output.json │ │ │ │ ├── permissions.go │ │ │ │ └── permissions.yaml │ │ │ ├── bitbucket/ │ │ │ │ ├── bitbucket.go │ │ │ │ ├── bitbucket_test.go │ │ │ │ ├── expected_output.json │ │ │ │ ├── permissions.go │ │ │ │ ├── permissions.yaml │ │ │ │ └── scopes.go │ │ │ ├── client.go │ │ │ ├── client_test.go │ │ │ ├── databricks/ │ │ │ │ ├── databricks.go │ │ │ │ ├── databricks_test.go │ │ │ │ ├── models.go │ │ │ │ ├── permissions.go │ │ │ │ ├── permissions.yaml │ │ │ │ ├── requests.go │ │ │ │ └── result_output.json │ │ │ ├── datadog/ │ │ │ │ ├── datadog.go │ │ │ │ ├── datadog_test.go │ │ │ │ ├── expected_output.json │ │ │ │ ├── expected_output_apikey.json │ │ │ │ ├── models.go │ │ │ │ ├── permissions.yaml │ │ │ │ ├── requests.go │ │ │ │ └── scopes.json │ │ │ ├── digitalocean/ │ │ │ │ ├── digitalocean.go │ │ │ │ ├── digitalocean_test.go │ │ │ │ ├── expected_output.json │ │ │ │ ├── permissions.go │ │ │ │ ├── permissions.yaml │ │ │ │ └── scopes.json │ │ │ ├── dockerhub/ │ │ │ │ ├── dockerhub.go │ │ │ │ ├── dockerhub_test.go │ │ │ │ ├── helper.go │ │ │ │ ├── permissions.go │ │ │ │ ├── permissions.yaml │ │ │ │ ├── requests.go │ │ │ │ └── result_output.json │ │ │ ├── dropbox/ │ │ │ │ ├── dropbox.go │ │ │ │ ├── dropbox_test.go │ │ │ │ ├── expected_output.json │ │ │ │ ├── models.go │ │ │ │ ├── permissions.go │ │ │ │ ├── permissions.yaml │ │ │ │ └── scopes.json │ │ │ ├── elevenlabs/ │ │ │ │ ├── elevenlabs.go │ │ │ │ ├── elevenlabs_test.go │ │ │ │ ├── permissions.go │ │ │ │ ├── permissions.yaml │ │ │ │ ├── requests.go │ │ │ │ └── result_output.json │ │ │ ├── fastly/ │ │ │ │ ├── fastly.go │ │ │ │ ├── fastly_test.go │ │ │ │ ├── models.go │ │ │ │ ├── permissions.go │ │ │ │ ├── permissions.yaml │ │ │ │ ├── requests.go │ │ │ │ └── result_output.json │ │ │ ├── figma/ │ │ │ │ ├── endpoints.json │ │ │ │ ├── expected_output.json │ │ │ │ ├── figma.go │ │ │ │ ├── figma_test.go │ │ │ │ ├── models.go │ │ │ │ ├── permissions.go │ │ │ │ ├── permissions.yaml │ │ │ │ ├── requests.go │ │ │ │ └── scopes.go │ │ │ ├── github/ │ │ │ │ ├── classic/ │ │ │ │ │ ├── classic.yaml │ │ │ │ │ ├── classic_permissions.go │ │ │ │ │ └── classictoken.go │ │ │ │ ├── common/ │ │ │ │ │ └── github.go │ │ │ │ ├── finegrained/ │ │ │ │ │ ├── finegrained.go │ │ │ │ │ ├── finegrained.yaml │ │ │ │ │ ├── finegrained_permissions.go │ │ │ │ │ └── finegrained_test.go │ │ │ │ ├── github.go │ │ │ │ └── github_test.go │ │ │ ├── gitlab/ │ │ │ │ ├── expected_output.json │ │ │ │ ├── gitlab.go │ │ │ │ ├── gitlab_test.go │ │ │ │ ├── permissions.go │ │ │ │ ├── permissions.yaml │ │ │ │ └── scopes.go │ │ │ ├── groq/ │ │ │ │ ├── groq.go │ │ │ │ ├── groq_test.go │ │ │ │ ├── permissions.go │ │ │ │ ├── permissions.yaml │ │ │ │ └── requests.go │ │ │ ├── huggingface/ │ │ │ │ ├── huggingface.go │ │ │ │ ├── huggingface_test.go │ │ │ │ ├── permissions.go │ │ │ │ ├── permissions.yaml │ │ │ │ └── scopes.go │ │ │ ├── jira/ │ │ │ │ ├── jira.go │ │ │ │ ├── jira_test.go │ │ │ │ ├── models.go │ │ │ │ ├── permissions.go │ │ │ │ ├── permissions.yaml │ │ │ │ ├── requests.go │ │ │ │ └── result_output.json │ │ │ ├── launchdarkly/ │ │ │ │ ├── launchdarkly.go │ │ │ │ ├── launchdarkly_test.go │ │ │ │ ├── models.go │ │ │ │ ├── permissions.go │ │ │ │ ├── permissions.yaml │ │ │ │ ├── requests.go │ │ │ │ ├── result_output.json │ │ │ │ └── user.go │ │ │ ├── mailchimp/ │ │ │ │ ├── expected_output.json │ │ │ │ ├── mailchimp.go │ │ │ │ ├── mailchimp_test.go │ │ │ │ ├── permissions.go │ │ │ │ └── permissions.yaml │ │ │ ├── mailgun/ │ │ │ │ ├── expected_output.json │ │ │ │ ├── mailgun.go │ │ │ │ ├── mailgun_test.go │ │ │ │ ├── permissions.go │ │ │ │ ├── permissions.yaml │ │ │ │ └── requests.go │ │ │ ├── monday/ │ │ │ │ ├── monday.go │ │ │ │ ├── monday_test.go │ │ │ │ ├── permissions.go │ │ │ │ ├── permissions.yaml │ │ │ │ ├── query.go │ │ │ │ ├── query.graphql │ │ │ │ └── result_output.json │ │ │ ├── mux/ │ │ │ │ ├── expected_output.json │ │ │ │ ├── models.go │ │ │ │ ├── mux.go │ │ │ │ ├── mux_test.go │ │ │ │ ├── permissions.go │ │ │ │ ├── permissions.yaml │ │ │ │ ├── requests.go │ │ │ │ ├── resources.go │ │ │ │ └── tests.json │ │ │ ├── mysql/ │ │ │ │ ├── expected_output.json │ │ │ │ ├── mysql.go │ │ │ │ ├── mysql_test.go │ │ │ │ ├── permissions.go │ │ │ │ ├── permissions.yaml │ │ │ │ └── scopes.go │ │ │ ├── netlify/ │ │ │ │ ├── models.go │ │ │ │ ├── netlify.go │ │ │ │ ├── netlify_test.go │ │ │ │ ├── permissions.go │ │ │ │ ├── permissions.yaml │ │ │ │ ├── requests.go │ │ │ │ └── result_output.json │ │ │ ├── ngrok/ │ │ │ │ ├── expected_output.json │ │ │ │ ├── models.go │ │ │ │ ├── ngrok.go │ │ │ │ ├── ngrok_test.go │ │ │ │ ├── permissions.go │ │ │ │ ├── permissions.yaml │ │ │ │ ├── requests.go │ │ │ │ └── resources.go │ │ │ ├── notion/ │ │ │ │ ├── expected_output.json │ │ │ │ ├── notion.go │ │ │ │ ├── notion_test.go │ │ │ │ ├── permissions.go │ │ │ │ ├── permissions.yaml │ │ │ │ └── scopes.json │ │ │ ├── openai/ │ │ │ │ ├── openai.go │ │ │ │ ├── openai_test.go │ │ │ │ ├── permissions.go │ │ │ │ ├── permissions.yaml │ │ │ │ ├── result_output.json │ │ │ │ └── scopes.go │ │ │ ├── opsgenie/ │ │ │ │ ├── opsgenie.go │ │ │ │ ├── opsgenie_test.go │ │ │ │ ├── permissions.go │ │ │ │ ├── permissions.yaml │ │ │ │ └── scopes.json │ │ │ ├── plaid/ │ │ │ │ ├── expected_output.json │ │ │ │ ├── models.go │ │ │ │ ├── permissions.go │ │ │ │ ├── permissions.yaml │ │ │ │ ├── plaid.go │ │ │ │ ├── plaid_test.go │ │ │ │ └── products.go │ │ │ ├── planetscale/ │ │ │ │ ├── expected_output.json │ │ │ │ ├── permissions.go │ │ │ │ ├── permissions.yaml │ │ │ │ ├── planetscale.go │ │ │ │ ├── planetscale_test.go │ │ │ │ └── scopes.json │ │ │ ├── postgres/ │ │ │ │ ├── expected_output.json │ │ │ │ ├── permissions.go │ │ │ │ ├── permissions.yaml │ │ │ │ ├── postgres.go │ │ │ │ └── postgres_test.go │ │ │ ├── posthog/ │ │ │ │ ├── expected_output.json │ │ │ │ ├── permissions.go │ │ │ │ ├── permissions.yaml │ │ │ │ ├── posthog.go │ │ │ │ ├── posthog_test.go │ │ │ │ └── scopes.json │ │ │ ├── postman/ │ │ │ │ ├── expected_output.json │ │ │ │ ├── permissions.go │ │ │ │ ├── permissions.yaml │ │ │ │ ├── postman.go │ │ │ │ ├── postman_test.go │ │ │ │ └── scopes.go │ │ │ ├── privatekey/ │ │ │ │ ├── expected_output.json │ │ │ │ ├── permissions.go │ │ │ │ ├── permissions.yaml │ │ │ │ ├── privatekey.go │ │ │ │ └── privatekey_test.go │ │ │ ├── sendgrid/ │ │ │ │ ├── permissions.go │ │ │ │ ├── permissions.yaml │ │ │ │ ├── result_output.json │ │ │ │ ├── scopes.go │ │ │ │ ├── sendgrid.go │ │ │ │ └── sendgrid_test.go │ │ │ ├── shopify/ │ │ │ │ ├── expected_output.json │ │ │ │ ├── permissions.go │ │ │ │ ├── permissions.yaml │ │ │ │ ├── scopes.json │ │ │ │ ├── shopify.go │ │ │ │ └── shopify_test.go │ │ │ ├── slack/ │ │ │ │ ├── expected_output.json │ │ │ │ ├── permissions.go │ │ │ │ ├── permissions.yaml │ │ │ │ ├── scopes.go │ │ │ │ ├── slack.go │ │ │ │ └── slack_test.go │ │ │ ├── sourcegraph/ │ │ │ │ ├── permissions.go │ │ │ │ ├── permissions.yaml │ │ │ │ ├── sourcegraph.go │ │ │ │ └── sourcegraph_test.go │ │ │ ├── square/ │ │ │ │ ├── expected_output.json │ │ │ │ ├── permissions.go │ │ │ │ ├── permissions.yaml │ │ │ │ ├── scopes.go │ │ │ │ ├── square.go │ │ │ │ └── square_test.go │ │ │ ├── stripe/ │ │ │ │ ├── expected_output.json │ │ │ │ ├── permissions.go │ │ │ │ ├── permissions.yaml │ │ │ │ ├── restricted.yaml │ │ │ │ ├── stripe.go │ │ │ │ └── stripe_test.go │ │ │ └── twilio/ │ │ │ ├── permissions.go │ │ │ ├── permissions.yaml │ │ │ ├── twilio.go │ │ │ └── twilio_test.go │ │ ├── cli.go │ │ ├── config/ │ │ │ └── config.go │ │ └── generate_permissions/ │ │ └── generate_permissions.go │ ├── buffers/ │ │ ├── buffer/ │ │ │ ├── buffer.go │ │ │ ├── buffer_test.go │ │ │ └── metrics.go │ │ └── pool/ │ │ ├── metrics.go │ │ ├── pool.go │ │ └── pool_test.go │ ├── cache/ │ │ ├── cache.go │ │ ├── decorator.go │ │ ├── decorator_test.go │ │ ├── lru/ │ │ │ ├── lru.go │ │ │ └── lru_test.go │ │ ├── metrics.go │ │ └── simple/ │ │ ├── simple.go │ │ └── simple_test.go │ ├── channelmetrics/ │ │ ├── metrics_collector/ │ │ │ └── prometheus/ │ │ │ └── collector.go │ │ ├── noopcollector.go │ │ ├── observablechan.go │ │ └── observablechan_test.go │ ├── cleantemp/ │ │ ├── cleantemp.go │ │ └── cleantemp_test.go │ ├── common/ │ │ ├── context.go │ │ ├── export_error.go │ │ ├── filter.go │ │ ├── filter_test.go │ │ ├── glob/ │ │ │ ├── glob.go │ │ │ └── glob_test.go │ │ ├── http.go │ │ ├── http_metrics.go │ │ ├── http_test.go │ │ ├── metrics.go │ │ ├── patterns.go │ │ ├── patterns_test.go │ │ ├── recover.go │ │ ├── secrets.go │ │ ├── utils.go │ │ ├── utils_test.go │ │ ├── vars.go │ │ └── vars_test.go │ ├── config/ │ │ ├── config.go │ │ ├── detectors.go │ │ └── detectors_test.go │ ├── context/ │ │ ├── context.go │ │ └── context_test.go │ ├── custom_detectors/ │ │ ├── CUSTOM_DETECTORS.md │ │ ├── custom_detectors.go │ │ ├── custom_detectors_test.go │ │ ├── regex_varstring.go │ │ ├── regex_varstring_test.go │ │ ├── validation.go │ │ └── validation_test.go │ ├── decoders/ │ │ ├── base64.go │ │ ├── base64_test.go │ │ ├── decoders.go │ │ ├── escaped_unicode.go │ │ ├── escaped_unicode_bench_test.go │ │ ├── escaped_unicode_test.go │ │ ├── utf16.go │ │ ├── utf16_test.go │ │ ├── utf8.go │ │ └── utf8_test.go │ ├── detectors/ │ │ ├── abstract/ │ │ │ ├── abstract.go │ │ │ ├── abstract_integration_test.go │ │ │ └── abstract_test.go │ │ ├── abuseipdb/ │ │ │ ├── abuseipdb.go │ │ │ ├── abuseipdb_integration_test.go │ │ │ └── abuseipdb_test.go │ │ ├── abyssale/ │ │ │ ├── abyssale.go │ │ │ ├── abyssale_integration_test.go │ │ │ └── abyssale_test.go │ │ ├── account_filter.go │ │ ├── account_filter_test.go │ │ ├── accuweather/ │ │ │ ├── v1/ │ │ │ │ ├── accuweather.go │ │ │ │ ├── accuweather_integration_test.go │ │ │ │ └── accuweather_test.go │ │ │ └── v2/ │ │ │ ├── accuweather.go │ │ │ ├── accuweather_integration_test.go │ │ │ └── accuweather_test.go │ │ ├── adafruitio/ │ │ │ ├── adafruitio.go │ │ │ ├── adafruitio_integration_test.go │ │ │ └── adafruitio_test.go │ │ ├── adobeio/ │ │ │ ├── adobeio.go │ │ │ ├── adobeio_integration_test.go │ │ │ └── adobeio_test.go │ │ ├── adzuna/ │ │ │ ├── adzuna.go │ │ │ ├── adzuna_integration_test.go │ │ │ └── adzuna_test.go │ │ ├── aeroworkflow/ │ │ │ ├── aeroworkflow.go │ │ │ ├── aeroworkflow_integration_test.go │ │ │ └── aeroworkflow_test.go │ │ ├── agora/ │ │ │ ├── agora.go │ │ │ ├── agora_integration_test.go │ │ │ └── agora_test.go │ │ ├── aha/ │ │ │ ├── aha.go │ │ │ ├── aha_integration_test.go │ │ │ └── aha_test.go │ │ ├── airbrakeprojectkey/ │ │ │ ├── airbrakeprojectkey.go │ │ │ ├── airbrakeprojectkey_integration_test.go │ │ │ └── airbrakeprojectkey_test.go │ │ ├── airbrakeuserkey/ │ │ │ ├── airbrakeuserkey.go │ │ │ ├── airbrakeuserkey_integration_test.go │ │ │ └── airbrakeuserkey_test.go │ │ ├── airship/ │ │ │ ├── airship.go │ │ │ ├── airship_integration_test.go │ │ │ └── airship_test.go │ │ ├── airtableoauth/ │ │ │ ├── airtableoauth.go │ │ │ ├── airtableoauth_integration_test.go │ │ │ └── airtableoauth_test.go │ │ ├── airtablepersonalaccesstoken/ │ │ │ ├── airtablepersonalaccesstoken.go │ │ │ ├── airtablepersonalaccesstoken_integration_test.go │ │ │ └── airtablepersonalaccesstoken_test.go │ │ ├── airvisual/ │ │ │ ├── airvisual.go │ │ │ ├── airvisual_integration_test.go │ │ │ └── airvisual_test.go │ │ ├── aiven/ │ │ │ ├── aiven.go │ │ │ ├── aiven_integration_test.go │ │ │ └── aiven_test.go │ │ ├── alchemy/ │ │ │ ├── alchemy.go │ │ │ ├── alchemy_integration_test.go │ │ │ └── alchemy_test.go │ │ ├── alconost/ │ │ │ ├── alconost.go │ │ │ ├── alconost_integration_test.go │ │ │ └── alconost_test.go │ │ ├── alegra/ │ │ │ ├── alegra.go │ │ │ ├── alegra_integration_test.go │ │ │ └── alegra_test.go │ │ ├── aletheiaapi/ │ │ │ ├── aletheiaapi.go │ │ │ ├── aletheiaapi_integration_test.go │ │ │ └── aletheiaapi_test.go │ │ ├── algoliaadminkey/ │ │ │ ├── algoliaadminkey.go │ │ │ ├── algoliaadminkey_integration_test.go │ │ │ └── algoliaadminkey_test.go │ │ ├── alibaba/ │ │ │ ├── alibaba.go │ │ │ ├── alibaba_integration_test.go │ │ │ └── alibaba_test.go │ │ ├── alienvault/ │ │ │ ├── alienvault.go │ │ │ ├── alienvault_integration_test.go │ │ │ └── alienvault_test.go │ │ ├── allsports/ │ │ │ ├── allsports.go │ │ │ ├── allsports_integration_test.go │ │ │ └── allsports_test.go │ │ ├── amadeus/ │ │ │ ├── amadeus.go │ │ │ ├── amadeus_integration_test.go │ │ │ └── amadeus_test.go │ │ ├── ambee/ │ │ │ ├── ambee.go │ │ │ ├── ambee_integration_test.go │ │ │ └── ambee_test.go │ │ ├── amplitudeapikey/ │ │ │ ├── amplitudeapikey.go │ │ │ ├── amplitudeapikey_integration_test.go │ │ │ └── amplitudeapikey_test.go │ │ ├── anthropic/ │ │ │ ├── anthropic.go │ │ │ ├── anthropic_integration_test.go │ │ │ └── anthropic_test.go │ │ ├── anypoint/ │ │ │ ├── anypoint.go │ │ │ ├── anypoint_integration_test.go │ │ │ └── anypoint_test.go │ │ ├── anypointoauth2/ │ │ │ ├── anypointoauth2.go │ │ │ ├── anypointoauth2_integration_test.go │ │ │ └── anypointoauth2_test.go │ │ ├── apacta/ │ │ │ ├── apacta.go │ │ │ ├── apacta_integration_test.go │ │ │ └── apacta_test.go │ │ ├── api2cart/ │ │ │ ├── api2cart.go │ │ │ ├── api2cart_integration_test.go │ │ │ └── api2cart_test.go │ │ ├── apideck/ │ │ │ ├── apideck.go │ │ │ ├── apideck_integration_test.go │ │ │ └── apideck_test.go │ │ ├── apiflash/ │ │ │ ├── apiflash.go │ │ │ ├── apiflash_integration_test.go │ │ │ └── apiflash_test.go │ │ ├── apifonica/ │ │ │ ├── apifonica.go │ │ │ ├── apifonica_integration_test.go │ │ │ └── apifonica_test.go │ │ ├── apify/ │ │ │ ├── apify.go │ │ │ ├── apify_integration_test.go │ │ │ └── apify_test.go │ │ ├── apilayer/ │ │ │ ├── apilayer.go │ │ │ ├── apilayer_integration_test.go │ │ │ └── apilayer_test.go │ │ ├── apimatic/ │ │ │ ├── apimatic.go │ │ │ ├── apimatic_integration_test.go │ │ │ └── apimatic_test.go │ │ ├── apimetrics/ │ │ │ ├── apimetrics.go │ │ │ ├── apimetrics_integration_test.go │ │ │ └── apimetrics_test.go │ │ ├── apitemplate/ │ │ │ ├── apitemplate.go │ │ │ ├── apitemplate_integration_test.go │ │ │ └── apitemplate_test.go │ │ ├── apollo/ │ │ │ ├── apollo.go │ │ │ ├── apollo_integration_test.go │ │ │ └── apollo_test.go │ │ ├── appcues/ │ │ │ ├── appcues.go │ │ │ ├── appcues_integration_test.go │ │ │ └── appcues_test.go │ │ ├── appfollow/ │ │ │ ├── appfollow.go │ │ │ ├── appfollow_integration_test.go │ │ │ └── appfollow_test.go │ │ ├── appointedd/ │ │ │ ├── appointedd.go │ │ │ ├── appointedd_integration_test.go │ │ │ └── appointedd_test.go │ │ ├── appoptics/ │ │ │ ├── appoptics.go │ │ │ ├── appoptics_integration_test.go │ │ │ └── appoptics_test.go │ │ ├── appsynergy/ │ │ │ ├── appsynergy.go │ │ │ ├── appsynergy_integration_test.go │ │ │ └── appsynergy_test.go │ │ ├── apptivo/ │ │ │ ├── apptivo.go │ │ │ ├── apptivo_integration_test.go │ │ │ └── apptivo_test.go │ │ ├── artifactory/ │ │ │ ├── artifactory.go │ │ │ ├── artifactory_integration_test.go │ │ │ └── artifactory_test.go │ │ ├── artifactoryreferencetoken/ │ │ │ ├── artifactoryreferencetoken.go │ │ │ ├── artifactoryreferencetoken_integration_test.go │ │ │ └── artifactoryreferencetoken_test.go │ │ ├── artsy/ │ │ │ ├── artsy.go │ │ │ ├── artsy_integration_test.go │ │ │ └── artsy_test.go │ │ ├── asanaoauth/ │ │ │ ├── asanaoauth.go │ │ │ ├── asanaoauth_integration_test.go │ │ │ └── asanaoauth_test.go │ │ ├── asanapersonalaccesstoken/ │ │ │ ├── asanapersonalaccesstoken.go │ │ │ ├── asanapersonalaccesstoken_integration_test.go │ │ │ └── asanapersonalaccesstoken_test.go │ │ ├── assemblyai/ │ │ │ ├── assemblyai.go │ │ │ ├── assemblyai_integration_test.go │ │ │ └── assemblyai_test.go │ │ ├── atera/ │ │ │ ├── atera.go │ │ │ ├── atera_integration_test.go │ │ │ └── atera_test.go │ │ ├── atlassian/ │ │ │ ├── v1/ │ │ │ │ ├── atlassian.go │ │ │ │ ├── atlassian_integration_test.go │ │ │ │ └── atlassian_test.go │ │ │ └── v2/ │ │ │ ├── atlassian.go │ │ │ ├── atlassian_integration_test.go │ │ │ └── atlassian_test.go │ │ ├── audd/ │ │ │ ├── audd.go │ │ │ ├── audd_integration_test.go │ │ │ └── audd_test.go │ │ ├── auth0managementapitoken/ │ │ │ ├── auth0managementapitoken.go │ │ │ ├── auth0managementapitoken_integration_test.go │ │ │ └── auth0managementapitoken_test.go │ │ ├── auth0oauth/ │ │ │ ├── auth0oauth.go │ │ │ ├── auth0oauth_integeration_test.go │ │ │ └── auth0oauth_test.go │ │ ├── autodesk/ │ │ │ ├── autodesk.go │ │ │ ├── autodesk_integration_test.go │ │ │ └── autodesk_test.go │ │ ├── autoklose/ │ │ │ ├── autoklose.go │ │ │ ├── autoklose_integration_test.go │ │ │ └── autoklose_test.go │ │ ├── autopilot/ │ │ │ ├── autopilot.go │ │ │ ├── autopilot_integration_test.go │ │ │ └── autopilot_test.go │ │ ├── avazapersonalaccesstoken/ │ │ │ ├── avazapersonalaccesstoken.go │ │ │ ├── avazapersonalaccesstoken_integration_test.go │ │ │ └── avazapersonalaccesstoken_test.go │ │ ├── aviationstack/ │ │ │ ├── aviationstack.go │ │ │ ├── aviationstack_integration_test.go │ │ │ └── aviationstack_test.go │ │ ├── aws/ │ │ │ ├── access_keys/ │ │ │ │ ├── accesskey.go │ │ │ │ ├── accesskey_integration_test.go │ │ │ │ ├── accesskey_test.go │ │ │ │ └── canary.go │ │ │ ├── common.go │ │ │ ├── session_keys/ │ │ │ │ ├── sessionkey.go │ │ │ │ └── sessionkeys_test.go │ │ │ └── utils.go │ │ ├── axonaut/ │ │ │ ├── axonaut.go │ │ │ ├── axonaut_integration_test.go │ │ │ └── axonaut_test.go │ │ ├── aylien/ │ │ │ ├── aylien.go │ │ │ ├── aylien_integration_test.go │ │ │ └── aylien_test.go │ │ ├── ayrshare/ │ │ │ ├── ayrshare.go │ │ │ ├── ayrshare_integration_test.go │ │ │ └── ayrshare_test.go │ │ ├── azure_batch/ │ │ │ ├── azurebatch.go │ │ │ ├── azurebatch_integration_test.go │ │ │ └── azurebatch_test.go │ │ ├── azure_cosmosdb/ │ │ │ ├── azure_cosmosdb.go │ │ │ ├── azure_cosmosdb_integration_test.go │ │ │ ├── azure_cosmosdb_test.go │ │ │ └── table.go │ │ ├── azure_entra/ │ │ │ ├── common.go │ │ │ ├── common_test.go │ │ │ ├── refreshtoken/ │ │ │ │ ├── refreshtoken.go │ │ │ │ ├── refreshtoken_integration_test.go │ │ │ │ └── refreshtoken_test.go │ │ │ └── serviceprincipal/ │ │ │ ├── sp.go │ │ │ ├── v1/ │ │ │ │ ├── spv1.go │ │ │ │ ├── spv1_integration_test.go │ │ │ │ └── spv1_test.go │ │ │ └── v2/ │ │ │ ├── spv2.go │ │ │ ├── spv2_integration_test.go │ │ │ └── spv2_test.go │ │ ├── azure_openai/ │ │ │ ├── azure_openai.go │ │ │ ├── azure_openai_integration_test.go │ │ │ └── azure_openai_test.go │ │ ├── azure_storage/ │ │ │ ├── storage.go │ │ │ ├── storage_integration_test.go │ │ │ └── storage_test.go │ │ ├── azureapimanagement/ │ │ │ └── repositorykey/ │ │ │ ├── repositorykey.go │ │ │ ├── repositorykey_integration_test.go │ │ │ └── repositorykey_test.go │ │ ├── azureapimanagementsubscriptionkey/ │ │ │ ├── azureapimanagementsubscriptionkey.go │ │ │ ├── azureapimanagementsubscriptionkey_integration_test.go │ │ │ └── azureapimanagementsubscriptionkey_test.go │ │ ├── azureappconfigconnectionstring/ │ │ │ ├── azureappconfigconnectionstring.go │ │ │ ├── azureappconfigconnectionstring_integration_test.go │ │ │ └── azureappconfigconnectionstring_test.go │ │ ├── azurecontainerregistry/ │ │ │ ├── azurecontainerregistry.go │ │ │ ├── azurecontainerregistry_integration_test.go │ │ │ └── azurecontainerregistry_test.go │ │ ├── azuredevopspersonalaccesstoken/ │ │ │ ├── azuredevopspersonalaccesstoken.go │ │ │ ├── azuredevopspersonalaccesstoken_integration_test.go │ │ │ └── azuredevopspersonalaccesstoken_test.go │ │ ├── azuredirectmanagementkey/ │ │ │ ├── azuredirectmanagementkey.go │ │ │ ├── azuredirectmanagementkey_integration_test.go │ │ │ └── azuredirectmanagementkey_test.go │ │ ├── azurefunctionkey/ │ │ │ ├── azurefunctionkey.go │ │ │ ├── azurefunctionkey_integration_test.go │ │ │ └── azurefunctionkey_test.go │ │ ├── azuresastoken/ │ │ │ ├── azuresastoken.go │ │ │ ├── azuresastoken_integration_test.go │ │ │ └── azuresastoken_test.go │ │ ├── azuresearchadminkey/ │ │ │ ├── azuresearchadminkey.go │ │ │ ├── azuresearchadminkey_integration_test.go │ │ │ └── azuresearchadminkey_test.go │ │ ├── azuresearchquerykey/ │ │ │ ├── azuresearchquerykey.go │ │ │ ├── azuresearchquerykey_integration_test.go │ │ │ └── azuresearchquerykey_test.go │ │ ├── bannerbear/ │ │ │ ├── v1/ │ │ │ │ ├── bannerbear.go │ │ │ │ ├── bannerbear_integration_test.go │ │ │ │ └── bannerbear_test.go │ │ │ └── v2/ │ │ │ ├── bannerbear.go │ │ │ ├── bannerbear_integration_test.go │ │ │ └── bannerbear_test.go │ │ ├── baremetrics/ │ │ │ ├── baremetrics.go │ │ │ ├── baremetrics_integration_test.go │ │ │ └── baremetrics_test.go │ │ ├── beamer/ │ │ │ ├── beamer.go │ │ │ ├── beamer_integration_test.go │ │ │ └── beamer_test.go │ │ ├── beebole/ │ │ │ ├── beebole.go │ │ │ ├── beebole_integration_test.go │ │ │ └── beebole_test.go │ │ ├── besnappy/ │ │ │ ├── besnappy.go │ │ │ ├── besnappy_integration_test.go │ │ │ └── besnappy_test.go │ │ ├── besttime/ │ │ │ ├── besttime.go │ │ │ ├── besttime_integration_test.go │ │ │ └── besttime_test.go │ │ ├── betterstack/ │ │ │ ├── betterstack.go │ │ │ ├── betterstack_integration_test.go │ │ │ └── betterstack_test.go │ │ ├── billomat/ │ │ │ ├── billomat.go │ │ │ ├── billomat_integration_test.go │ │ │ └── billomat_test.go │ │ ├── bingsubscriptionkey/ │ │ │ ├── bingsubscriptionkey.go │ │ │ ├── bingsubscriptionkey_integration_test.go │ │ │ └── bingsubscriptionkey_test.go │ │ ├── bitbar/ │ │ │ ├── bitbar.go │ │ │ ├── bitbar_integration_test.go │ │ │ └── bitbar_test.go │ │ ├── bitbucketapppassword/ │ │ │ ├── bitbucketapppassword.go │ │ │ ├── bitbucketapppassword_integration_test.go │ │ │ └── bitbucketapppassword_test.go │ │ ├── bitcoinaverage/ │ │ │ ├── bitcoinaverage.go │ │ │ ├── bitcoinaverage_integration_test.go │ │ │ └── bitcoinaverage_test.go │ │ ├── bitfinex/ │ │ │ ├── bitfinex.go │ │ │ ├── bitfinex_integration_test.go │ │ │ └── bitfinex_test.go │ │ ├── bitlyaccesstoken/ │ │ │ ├── bitlyaccesstoken.go │ │ │ ├── bitlyaccesstoken_integration_test.go │ │ │ └── bitlyaccesstoken_test.go │ │ ├── bitmex/ │ │ │ ├── bitmex.go │ │ │ ├── bitmex_integration_test.go │ │ │ └── bitmex_test.go │ │ ├── blazemeter/ │ │ │ ├── blazemeter.go │ │ │ ├── blazemeter_integration_test.go │ │ │ └── blazemeter_test.go │ │ ├── blitapp/ │ │ │ ├── blitapp.go │ │ │ ├── blitapp_integration_test.go │ │ │ └── blitapp_test.go │ │ ├── blocknative/ │ │ │ ├── blocknative.go │ │ │ ├── blocknative_integration_test.go │ │ │ └── blocknative_test.go │ │ ├── blogger/ │ │ │ ├── blogger.go │ │ │ ├── blogger_integration_test.go │ │ │ └── blogger_test.go │ │ ├── bombbomb/ │ │ │ ├── bombbomb.go │ │ │ ├── bombbomb_integration_test.go │ │ │ └── bombbomb_test.go │ │ ├── boostnote/ │ │ │ ├── boostnote.go │ │ │ ├── boostnote_integration_test.go │ │ │ └── boostnote_test.go │ │ ├── borgbase/ │ │ │ ├── borgbase.go │ │ │ ├── borgbase_integration_test.go │ │ │ └── borgbase_test.go │ │ ├── box/ │ │ │ ├── box.go │ │ │ ├── box_integration_test.go │ │ │ └── box_test.go │ │ ├── boxoauth/ │ │ │ ├── boxoauth.go │ │ │ ├── boxoauth_integration_test.go │ │ │ └── boxoauth_test.go │ │ ├── braintreepayments/ │ │ │ ├── braintreepayments.go │ │ │ ├── braintreepayments_integration_test.go │ │ │ └── braintreepayments_test.go │ │ ├── brandfetch/ │ │ │ ├── v1/ │ │ │ │ ├── brandfetch.go │ │ │ │ ├── brandfetch_integration_test.go │ │ │ │ └── brandfetch_test.go │ │ │ └── v2/ │ │ │ ├── brandfetch.go │ │ │ ├── brandfetch_integration_test.go │ │ │ └── brandfetch_test.go │ │ ├── browserstack/ │ │ │ ├── browserstack.go │ │ │ ├── browserstack_integration_test.go │ │ │ └── browserstack_test.go │ │ ├── browshot/ │ │ │ ├── browshot.go │ │ │ ├── browshot_integration_test.go │ │ │ └── browshot_test.go │ │ ├── bscscan/ │ │ │ ├── bscscan.go │ │ │ ├── bscscan_integration_test.go │ │ │ └── bscscan_test.go │ │ ├── buddyns/ │ │ │ ├── buddyns.go │ │ │ ├── buddyns_integration_test.go │ │ │ └── buddyns_test.go │ │ ├── budibase/ │ │ │ ├── budibase.go │ │ │ ├── budibase_integration_test.go │ │ │ └── budibase_test.go │ │ ├── bugherd/ │ │ │ ├── bugherd.go │ │ │ ├── bugherd_integration_test.go │ │ │ └── bugherd_test.go │ │ ├── bugsnag/ │ │ │ ├── bugsnag.go │ │ │ ├── bugsnag_integration_test.go │ │ │ └── bugsnag_test.go │ │ ├── buildkite/ │ │ │ ├── v1/ │ │ │ │ ├── buildkite.go │ │ │ │ ├── buildkite_integration_test.go │ │ │ │ └── buildkite_test.go │ │ │ └── v2/ │ │ │ ├── buildkite.go │ │ │ ├── buildkite_test.go │ │ │ └── buildkitev2_integration_test.go │ │ ├── bulbul/ │ │ │ ├── bulbul.go │ │ │ ├── bulbul_integration_test.go │ │ │ └── bulbul_test.go │ │ ├── bulksms/ │ │ │ ├── bulksms.go │ │ │ ├── bulksms_integration_test.go │ │ │ └── bulksms_test.go │ │ ├── buttercms/ │ │ │ ├── buttercms.go │ │ │ ├── buttercms_integration_test.go │ │ │ └── buttercms_test.go │ │ ├── caflou/ │ │ │ ├── caflou.go │ │ │ ├── caflou_integration_test.go │ │ │ └── caflou_test.go │ │ ├── calendarific/ │ │ │ ├── calendarific.go │ │ │ ├── calendarific_integration_test.go │ │ │ └── calendarific_test.go │ │ ├── calendlyapikey/ │ │ │ ├── calendlyapikey.go │ │ │ ├── calendlyapikey_integration_test.go │ │ │ └── calendlyapikey_test.go │ │ ├── calorieninja/ │ │ │ ├── calorieninja.go │ │ │ ├── calorieninja_integration_test.go │ │ │ └── calorieninja_test.go │ │ ├── campayn/ │ │ │ ├── campayn.go │ │ │ ├── campayn_integration_test.go │ │ │ └── campayn_test.go │ │ ├── cannyio/ │ │ │ ├── cannyio.go │ │ │ ├── cannyio_integration_test.go │ │ │ └── cannyio_test.go │ │ ├── capsulecrm/ │ │ │ ├── capsulecrm.go │ │ │ ├── capsulecrm_integration_test.go │ │ │ └── capsulecrm_test.go │ │ ├── captaindata/ │ │ │ ├── v1/ │ │ │ │ ├── captaindata.go │ │ │ │ ├── captaindata_integration_test.go │ │ │ │ └── captaindata_test.go │ │ │ └── v2/ │ │ │ ├── captaindata.go │ │ │ ├── captaindata_integration_test.go │ │ │ └── captaindata_test.go │ │ ├── carboninterface/ │ │ │ ├── carboninterface.go │ │ │ ├── carboninterface_integration_test.go │ │ │ └── carboninterface_test.go │ │ ├── cashboard/ │ │ │ ├── cashboard.go │ │ │ ├── cashboard_integration_test.go │ │ │ └── cashboard_test.go │ │ ├── caspio/ │ │ │ ├── caspio.go │ │ │ ├── caspio_integration_test.go │ │ │ └── caspio_test.go │ │ ├── censys/ │ │ │ ├── censys.go │ │ │ ├── censys_integration_test.go │ │ │ └── censys_test.go │ │ ├── centralstationcrm/ │ │ │ ├── centralstationcrm.go │ │ │ ├── centralstationcrm_integration_test.go │ │ │ └── centralstationcrm_test.go │ │ ├── cexio/ │ │ │ ├── cexio.go │ │ │ ├── cexio_integration_test.go │ │ │ └── cexio_test.go │ │ ├── chartmogul/ │ │ │ ├── chartmogul.go │ │ │ ├── chartmogul_integration_test.go │ │ │ └── chartmogul_test.go │ │ ├── chatbot/ │ │ │ ├── chatbot.go │ │ │ ├── chatbot_integration_test.go │ │ │ └── chatbot_test.go │ │ ├── chatfule/ │ │ │ ├── chatfule.go │ │ │ ├── chatfule_integration_test.go │ │ │ └── chatfule_test.go │ │ ├── checio/ │ │ │ ├── checio.go │ │ │ ├── checio_integration_test.go │ │ │ └── checio_test.go │ │ ├── checklyhq/ │ │ │ ├── checklyhq.go │ │ │ ├── checklyhq_integration_test.go │ │ │ └── checklyhq_test.go │ │ ├── checkout/ │ │ │ ├── checkout.go │ │ │ ├── checkout_integration_test.go │ │ │ └── checkout_test.go │ │ ├── checkvist/ │ │ │ ├── checkvist.go │ │ │ ├── checkvist_integration_test.go │ │ │ └── checkvist_test.go │ │ ├── cicero/ │ │ │ ├── cicero.go │ │ │ ├── cicero_integration_test.go │ │ │ └── cicero_test.go │ │ ├── circleci/ │ │ │ ├── v1/ │ │ │ │ ├── circleci.go │ │ │ │ ├── circleci_integration_test.go │ │ │ │ └── circleci_test.go │ │ │ └── v2/ │ │ │ ├── circleci.go │ │ │ ├── circleci_integration_test.go │ │ │ └── circleci_test.go │ │ ├── clarifai/ │ │ │ ├── clarifai.go │ │ │ ├── clarifai_integration_test.go │ │ │ └── clarifai_test.go │ │ ├── clearbit/ │ │ │ ├── clearbit.go │ │ │ ├── clearbit_integration_test.go │ │ │ └── clearbit_test.go │ │ ├── clickhelp/ │ │ │ ├── clickhelp.go │ │ │ ├── clickhelp_integration_test.go │ │ │ └── clickhelp_test.go │ │ ├── clicksendsms/ │ │ │ ├── clicksendsms.go │ │ │ ├── clicksendsms_integration_test.go │ │ │ └── clicksendsms_test.go │ │ ├── clickuppersonaltoken/ │ │ │ ├── clickuppersonaltoken.go │ │ │ ├── clickuppersonaltoken_integration_test.go │ │ │ └── clickuppersonaltoken_test.go │ │ ├── cliengo/ │ │ │ ├── cliengo.go │ │ │ ├── cliengo_integration_test.go │ │ │ └── cliengo_test.go │ │ ├── clientary/ │ │ │ ├── clientary.go │ │ │ ├── clientary_integration_test.go │ │ │ └── clientary_test.go │ │ ├── clinchpad/ │ │ │ ├── clinchpad.go │ │ │ ├── clinchpad_integration_test.go │ │ │ └── clinchpad_test.go │ │ ├── clockify/ │ │ │ ├── clockify.go │ │ │ ├── clockify_integration_test.go │ │ │ └── clockify_test.go │ │ ├── clockworksms/ │ │ │ ├── clockworksms.go │ │ │ ├── clockworksms_integration_test.go │ │ │ └── clockworksms_test.go │ │ ├── closecrm/ │ │ │ ├── close.go │ │ │ ├── close_integration_test.go │ │ │ └── close_test.go │ │ ├── cloudconvert/ │ │ │ ├── cloudconvert.go │ │ │ ├── cloudconvert_integration_test.go │ │ │ └── cloudconvert_test.go │ │ ├── cloudelements/ │ │ │ ├── cloudelements.go │ │ │ ├── cloudelements_integration_test.go │ │ │ └── cloudelements_test.go │ │ ├── cloudflareapitoken/ │ │ │ ├── cloudflareapitoken.go │ │ │ ├── cloudflareapitoken_integration_test.go │ │ │ └── cloudflareapitoken_test.go │ │ ├── cloudflarecakey/ │ │ │ ├── cloudflarecakey.go │ │ │ ├── cloudflarecakey_integration_test.go │ │ │ └── cloudflarecakey_test.go │ │ ├── cloudflareglobalapikey/ │ │ │ ├── cloudflareglobalapikey.go │ │ │ ├── cloudflareglobalapikey_integration_test.go │ │ │ └── cloudflareglobalapikey_test.go │ │ ├── cloudimage/ │ │ │ ├── cloudimage.go │ │ │ ├── cloudimage_integration_test.go │ │ │ └── cloudimage_test.go │ │ ├── cloudmersive/ │ │ │ ├── cloudmersive.go │ │ │ ├── cloudmersive_integration_test.go │ │ │ └── cloudmersive_test.go │ │ ├── cloudplan/ │ │ │ ├── cloudplan.go │ │ │ ├── cloudplan_integration_test.go │ │ │ └── cloudplan_test.go │ │ ├── cloudsmith/ │ │ │ ├── cloudsmith.go │ │ │ ├── cloudsmith_integration_test.go │ │ │ └── cloudsmith_test.go │ │ ├── cloverly/ │ │ │ ├── cloverly.go │ │ │ ├── cloverly_integration_test.go │ │ │ └── cloverly_test.go │ │ ├── cloze/ │ │ │ ├── cloze.go │ │ │ ├── cloze_integration_test.go │ │ │ └── cloze_test.go │ │ ├── clustdoc/ │ │ │ ├── clustdoc.go │ │ │ ├── clustdoc_integration_test.go │ │ │ └── clustdoc_test.go │ │ ├── coda/ │ │ │ ├── coda.go │ │ │ ├── coda_integration_test.go │ │ │ └── coda_test.go │ │ ├── codacy/ │ │ │ ├── codacy.go │ │ │ ├── codacy_integration_test.go │ │ │ └── codacy_test.go │ │ ├── codeclimate/ │ │ │ ├── codeclimate.go │ │ │ ├── codeclimate_integration_test.go │ │ │ └── codeclimate_test.go │ │ ├── codemagic/ │ │ │ ├── codemagic.go │ │ │ ├── codemagic_integration_test.go │ │ │ └── codemagic_test.go │ │ ├── codequiry/ │ │ │ ├── codequiry.go │ │ │ ├── codequiry_integration_test.go │ │ │ └── codequiry_test.go │ │ ├── coinapi/ │ │ │ ├── coinapi.go │ │ │ ├── coinapi_integration_test.go │ │ │ └── coinapi_test.go │ │ ├── coinbase/ │ │ │ ├── coinbase.go │ │ │ ├── coinbase_integration_test.go │ │ │ └── coinbase_test.go │ │ ├── coinlayer/ │ │ │ ├── coinlayer.go │ │ │ ├── coinlayer_integration_test.go │ │ │ └── coinlayer_test.go │ │ ├── coinlib/ │ │ │ ├── coinlib.go │ │ │ ├── coinlib_integration_test.go │ │ │ └── coinlib_test.go │ │ ├── collect2/ │ │ │ ├── collect2.go │ │ │ ├── collect2_integration_test.go │ │ │ └── collect2_test.go │ │ ├── column/ │ │ │ ├── column.go │ │ │ ├── column_integration_test.go │ │ │ └── column_test.go │ │ ├── commercejs/ │ │ │ ├── commercejs.go │ │ │ ├── commercejs_integration_test.go │ │ │ └── commercejs_test.go │ │ ├── commodities/ │ │ │ ├── commodities.go │ │ │ ├── commodities_integration_test.go │ │ │ └── commodities_test.go │ │ ├── companyhub/ │ │ │ ├── companyhub.go │ │ │ ├── companyhub_integration_test.go │ │ │ └── companyhub_test.go │ │ ├── confluent/ │ │ │ ├── confluent.go │ │ │ ├── confluent_integration_test.go │ │ │ └── confluent_test.go │ │ ├── contentfulpersonalaccesstoken/ │ │ │ ├── contentfulpersonalaccesstoken.go │ │ │ ├── contentfulpersonalaccesstoken_test.go │ │ │ └── contentfulpersonalacesstoken_integration_test.go │ │ ├── conversiontools/ │ │ │ ├── conversiontools.go │ │ │ ├── conversiontools_integration_test.go │ │ │ └── conversiontools_test.go │ │ ├── convertapi/ │ │ │ ├── convertapi.go │ │ │ ├── convertapi_integration_test.go │ │ │ └── convertapi_test.go │ │ ├── convertkit/ │ │ │ ├── convertkit.go │ │ │ ├── convertkit_integration_test.go │ │ │ └── convertkit_test.go │ │ ├── convier/ │ │ │ ├── convier.go │ │ │ ├── convier_integration_test.go │ │ │ └── convier_test.go │ │ ├── copper/ │ │ │ ├── copper.go │ │ │ ├── copper_integration_test.go │ │ │ └── copper_test.go │ │ ├── copy_metadata_test.go │ │ ├── couchbase/ │ │ │ ├── couchbase.go │ │ │ ├── couchbase_integration_test.go │ │ │ └── couchbase_test.go │ │ ├── countrylayer/ │ │ │ ├── countrylayer.go │ │ │ ├── countrylayer_integration_test.go │ │ │ └── countrylayer_test.go │ │ ├── courier/ │ │ │ ├── courier.go │ │ │ ├── courier_integration_test.go │ │ │ └── courier_test.go │ │ ├── coveralls/ │ │ │ ├── coveralls.go │ │ │ ├── coveralls_integration_test.go │ │ │ └── coveralls_test.go │ │ ├── craftmypdf/ │ │ │ ├── craftmypdf.go │ │ │ ├── craftmypdf_integration_test.go │ │ │ └── craftmypdf_test.go │ │ ├── crowdin/ │ │ │ ├── crowdin.go │ │ │ ├── crowdin_integration_test.go │ │ │ └── crowdin_test.go │ │ ├── cryptocompare/ │ │ │ ├── cryptocompare.go │ │ │ ├── cryptocompare_integration_test.go │ │ │ └── cryptocompare_test.go │ │ ├── currencycloud/ │ │ │ ├── currencycloud.go │ │ │ ├── currencycloud_integration_test.go │ │ │ └── currencycloud_test.go │ │ ├── currencyfreaks/ │ │ │ ├── currencyfreaks.go │ │ │ ├── currencyfreaks_integration_test.go │ │ │ └── currencyfreaks_test.go │ │ ├── currencylayer/ │ │ │ ├── currencylayer.go │ │ │ ├── currencylayer_integration_test.go │ │ │ └── currencylayer_test.go │ │ ├── currencyscoop/ │ │ │ ├── currencyscoop.go │ │ │ ├── currencyscoop_integration_test.go │ │ │ └── currencyscoop_test.go │ │ ├── currentsapi/ │ │ │ ├── currentsapi.go │ │ │ ├── currentsapi_integration_test.go │ │ │ └── currentsapi_test.go │ │ ├── customerguru/ │ │ │ ├── customerguru.go │ │ │ ├── customerguru_integration_test.go │ │ │ └── customerguru_test.go │ │ ├── customerio/ │ │ │ ├── customerio.go │ │ │ ├── customerio_integration_test.go │ │ │ └── customerio_test.go │ │ ├── d7network/ │ │ │ ├── d7network.go │ │ │ ├── d7network_integration_test.go │ │ │ └── d7network_test.go │ │ ├── dailyco/ │ │ │ ├── dailyco.go │ │ │ ├── dailyco_integration_test.go │ │ │ └── dailyco_test.go │ │ ├── dandelion/ │ │ │ ├── dandelion.go │ │ │ ├── dandelion_integration_test.go │ │ │ └── dandelion_test.go │ │ ├── dareboost/ │ │ │ ├── dareboost.go │ │ │ ├── dareboost_integration_test.go │ │ │ └── dareboost_test.go │ │ ├── databox/ │ │ │ ├── databox.go │ │ │ ├── databox_integration_test.go │ │ │ └── databox_test.go │ │ ├── databrickstoken/ │ │ │ ├── databrickstoken.go │ │ │ ├── databrickstoken_integration_test.go │ │ │ └── databrickstoken_test.go │ │ ├── datadogapikey/ │ │ │ ├── datadogapikey.go │ │ │ ├── datadogapikey_integration_test.go │ │ │ └── datadogapikey_test.go │ │ ├── datadogtoken/ │ │ │ ├── datadogtoken.go │ │ │ ├── datadogtoken_integration_test.go │ │ │ └── datadogtoken_test.go │ │ ├── datagov/ │ │ │ ├── datagov.go │ │ │ ├── datagov_integration_test.go │ │ │ └── datagov_test.go │ │ ├── debounce/ │ │ │ ├── debounce.go │ │ │ ├── debounce_integration_test.go │ │ │ └── debounce_test.go │ │ ├── deepai/ │ │ │ ├── deepai.go │ │ │ ├── deepai_integration_test.go │ │ │ └── deepai_test.go │ │ ├── deepgram/ │ │ │ ├── deepgram.go │ │ │ ├── deepgram_integration_test.go │ │ │ └── deepgram_test.go │ │ ├── deepseek/ │ │ │ ├── deepseek.go │ │ │ ├── deepseek_integration_test.go │ │ │ └── deepseek_test.go │ │ ├── delighted/ │ │ │ ├── delighted.go │ │ │ ├── delighted_integration_test.go │ │ │ └── delighted_test.go │ │ ├── demio/ │ │ │ ├── demio.go │ │ │ ├── demio_integration_test.go │ │ │ └── demio_test.go │ │ ├── deno/ │ │ │ ├── denodeploy.go │ │ │ ├── denodeploy_integration_test.go │ │ │ └── denodeploy_test.go │ │ ├── deputy/ │ │ │ ├── deputy.go │ │ │ ├── deputy_integration_test.go │ │ │ └── deputy_test.go │ │ ├── detectify/ │ │ │ ├── detectify.go │ │ │ ├── detectify_integration_test.go │ │ │ └── detectify_test.go │ │ ├── detectlanguage/ │ │ │ ├── detectlanguage.go │ │ │ ├── detectlanguage_integration_test.go │ │ │ └── detectlanguage_test.go │ │ ├── detectors.go │ │ ├── detectors_test.go │ │ ├── dfuse/ │ │ │ ├── dfuse.go │ │ │ ├── dfuse_integration_test.go │ │ │ └── dfuse_test.go │ │ ├── diffbot/ │ │ │ ├── diffbot.go │ │ │ ├── diffbot_integration_test.go │ │ │ └── diffbot_test.go │ │ ├── diggernaut/ │ │ │ ├── diggernaut.go │ │ │ ├── diggernaut_integration_test.go │ │ │ └── diggernaut_test.go │ │ ├── digitaloceantoken/ │ │ │ ├── digitaloceantoken.go │ │ │ ├── digitaloceantoken_integration_test.go │ │ │ └── digitaloceantoken_test.go │ │ ├── digitaloceanv2/ │ │ │ ├── digitaloceanv2.go │ │ │ ├── digitaloceanv2_integration_test.go │ │ │ └── digitaloceanv2_test.go │ │ ├── discordbottoken/ │ │ │ ├── discordbottoken.go │ │ │ ├── discordbottoken_integration_test.go │ │ │ └── discordbottoken_test.go │ │ ├── discordwebhook/ │ │ │ ├── discordwebhook.go │ │ │ ├── discordwebhook_integration_test.go │ │ │ └── discordwebhook_test.go │ │ ├── disqus/ │ │ │ ├── disqus.go │ │ │ ├── disqus_integration_test.go │ │ │ └── disqus_test.go │ │ ├── ditto/ │ │ │ ├── ditto.go │ │ │ ├── ditto_integration_test.go │ │ │ └── ditto_test.go │ │ ├── dnscheck/ │ │ │ ├── dnscheck.go │ │ │ ├── dnscheck_integration_test.go │ │ │ └── dnscheck_test.go │ │ ├── docker/ │ │ │ ├── docker_auth_config.go │ │ │ ├── docker_auth_config_integration_test.go │ │ │ └── docker_auth_config_test.go │ │ ├── dockerhub/ │ │ │ ├── v1/ │ │ │ │ ├── dockerhub.go │ │ │ │ ├── dockerhub_integration_test.go │ │ │ │ └── dockerhub_test.go │ │ │ └── v2/ │ │ │ ├── dockerhub.go │ │ │ ├── dockerhub_integration_test.go │ │ │ └── dockerhub_test.go │ │ ├── docparser/ │ │ │ ├── docparser.go │ │ │ ├── docparser_integration_test.go │ │ │ └── docparser_test.go │ │ ├── documo/ │ │ │ ├── documo.go │ │ │ ├── documo_integration_test.go │ │ │ └── documo_test.go │ │ ├── docusign/ │ │ │ ├── docusign.go │ │ │ ├── docusign_integration_test.go │ │ │ └── docusign_test.go │ │ ├── doppler/ │ │ │ ├── doppler.go │ │ │ ├── doppler_integration_test.go │ │ │ └── doppler_test.go │ │ ├── dotdigital/ │ │ │ ├── dotdigital.go │ │ │ ├── dotdigital_integration_test.go │ │ │ └── dotdigital_test.go │ │ ├── dovico/ │ │ │ ├── dovico.go │ │ │ ├── dovico_integration_test.go │ │ │ └── dovico_test.go │ │ ├── dronahq/ │ │ │ ├── dronahq.go │ │ │ ├── dronahq_integration_test.go │ │ │ └── dronahq_test.go │ │ ├── droneci/ │ │ │ ├── droneci.go │ │ │ ├── droneci_integration_test.go │ │ │ └── droneci_test.go │ │ ├── dropbox/ │ │ │ ├── dropbox.go │ │ │ ├── dropbox_integration_test.go │ │ │ └── dropbox_test.go │ │ ├── duply/ │ │ │ ├── duply.go │ │ │ ├── duply_integration_test.go │ │ │ └── duply_test.go │ │ ├── dwolla/ │ │ │ ├── dwolla.go │ │ │ ├── dwolla_integration_test.go │ │ │ └── dwolla_test.go │ │ ├── dynalist/ │ │ │ ├── dynalist.go │ │ │ ├── dynalist_integration_test.go │ │ │ └── dynalist_test.go │ │ ├── dyspatch/ │ │ │ ├── dyspatch.go │ │ │ ├── dyspatch_integration_test.go │ │ │ └── dyspatch_test.go │ │ ├── eagleeyenetworks/ │ │ │ ├── eagleeyenetworks.go │ │ │ ├── eagleeyenetworks_integration_test.go │ │ │ └── eagleeyenetworks_test.go │ │ ├── easyinsight/ │ │ │ ├── easyinsight.go │ │ │ ├── easyinsight_integration_test.go │ │ │ └── easyinsight_test.go │ │ ├── ecostruxureit/ │ │ │ ├── ecostruxureit.go │ │ │ ├── ecostruxureit_integration_test.go │ │ │ └── ecostruxureit_test.go │ │ ├── edamam/ │ │ │ ├── edamam.go │ │ │ ├── edamam_integration_test.go │ │ │ └── edamam_test.go │ │ ├── edenai/ │ │ │ ├── edenai.go │ │ │ ├── edenai_integration_test.go │ │ │ └── edenai_test.go │ │ ├── eightxeight/ │ │ │ ├── eightxeight.go │ │ │ ├── eightxeight_integration_test.go │ │ │ └── eightxeight_test.go │ │ ├── elasticemail/ │ │ │ ├── elasticemail.go │ │ │ ├── elasticemail_integration_test.go │ │ │ └── elasticemail_test.go │ │ ├── elevenlabs/ │ │ │ ├── v1/ │ │ │ │ ├── elevenlabs.go │ │ │ │ ├── elevenlabs_integration_test.go │ │ │ │ └── elevenlabs_test.go │ │ │ └── v2/ │ │ │ ├── elevenlabs.go │ │ │ ├── elevenlabs_integration_test.go │ │ │ └── elevenlabs_test.go │ │ ├── enablex/ │ │ │ ├── enablex.go │ │ │ ├── enablex_integration_test.go │ │ │ └── enablex_test.go │ │ ├── endorlabs/ │ │ │ ├── endorlabs.go │ │ │ ├── endorlabs_integration_test.go │ │ │ └── endorlabs_test.go │ │ ├── endpoint_customizer.go │ │ ├── endpoint_customizer_test.go │ │ ├── enigma/ │ │ │ ├── enigma.go │ │ │ ├── enigma_integration_test.go │ │ │ └── enigma_test.go │ │ ├── envoyapikey/ │ │ │ ├── envoyapikey.go │ │ │ ├── envoyapikey_integration_test.go │ │ │ └── envoyapikey_test.go │ │ ├── eraser/ │ │ │ ├── eraser.go │ │ │ ├── eraser_integration_test.go │ │ │ └── eraser_test.go │ │ ├── etherscan/ │ │ │ ├── etherscan.go │ │ │ ├── etherscan_integration_test.go │ │ │ └── etherscan_test.go │ │ ├── ethplorer/ │ │ │ ├── ethplorer.go │ │ │ ├── ethplorer_integration_test.go │ │ │ └── ethplorer_test.go │ │ ├── eventbrite/ │ │ │ ├── eventbrite.go │ │ │ ├── eventbrite_integration_test.go │ │ │ └── eventbrite_test.go │ │ ├── everhour/ │ │ │ ├── everhour.go │ │ │ ├── everhour_integration_test.go │ │ │ └── everhour_test.go │ │ ├── exchangerateapi/ │ │ │ ├── exchangerateapi.go │ │ │ ├── exchangerateapi_integration_test.go │ │ │ └── exchangerateapi_test.go │ │ ├── exchangeratesapi/ │ │ │ ├── exchangeratesapi.go │ │ │ ├── exchangeratesapi_integration_test.go │ │ │ └── exchangeratesapi_test.go │ │ ├── exportsdk/ │ │ │ ├── exportsdk.go │ │ │ ├── exportsdk_integration_test.go │ │ │ └── exportsdk_test.go │ │ ├── extractorapi/ │ │ │ ├── extractorapi.go │ │ │ ├── extractorapi_integration_test.go │ │ │ └── extractorapi_test.go │ │ ├── facebookoauth/ │ │ │ ├── facebookoauth.go │ │ │ ├── facebookoauth_integration_test.go │ │ │ └── facebookoauth_test.go │ │ ├── faceplusplus/ │ │ │ ├── faceplusplus.go │ │ │ ├── faceplusplus_integration_test.go │ │ │ └── faceplusplus_test.go │ │ ├── falsepositives.go │ │ ├── falsepositives_test.go │ │ ├── fastforex/ │ │ │ ├── fastforex.go │ │ │ ├── fastforex_integration_test.go │ │ │ └── fastforex_test.go │ │ ├── fastlypersonaltoken/ │ │ │ ├── fastlypersonaltoken.go │ │ │ ├── fastlypersonaltoken_integration_test.go │ │ │ └── fastlypersonaltoken_test.go │ │ ├── feedier/ │ │ │ ├── feedier.go │ │ │ ├── feedier_integration_test.go │ │ │ └── feedier_test.go │ │ ├── fetchrss/ │ │ │ ├── fetchrss.go │ │ │ ├── fetchrss_integration_test.go │ │ │ └── fetchrss_test.go │ │ ├── fibery/ │ │ │ ├── fibery.go │ │ │ ├── fibery_integration_test.go │ │ │ └── fibery_test.go │ │ ├── figmapersonalaccesstoken/ │ │ │ ├── v1/ │ │ │ │ ├── figmapersonalaccesstoken.go │ │ │ │ ├── figmapersonalaccesstoken_test.go │ │ │ │ └── figmapersonalacesstoken_integration_test.go │ │ │ └── v2/ │ │ │ ├── figmapersonalaccesstoken_integration_test.go │ │ │ ├── figmapersonalaccesstoken_v2.go │ │ │ └── figmapersonalaccesstoken_v2_test.go │ │ ├── fileio/ │ │ │ ├── fileio.go │ │ │ ├── fileio_integration_test.go │ │ │ └── fileio_test.go │ │ ├── finage/ │ │ │ ├── finage.go │ │ │ ├── finage_integration_test.go │ │ │ └── finage_test.go │ │ ├── financialmodelingprep/ │ │ │ ├── financialmodelingprep.go │ │ │ ├── financialmodelingprep_integration_test.go │ │ │ └── financialmodelingprep_test.go │ │ ├── findl/ │ │ │ ├── findl.go │ │ │ ├── findl_integration_test.go │ │ │ └── findl_test.go │ │ ├── finnhub/ │ │ │ ├── finnhub.go │ │ │ ├── finnhub_integration_test.go │ │ │ └── finnhub_test.go │ │ ├── fixerio/ │ │ │ ├── fixerio.go │ │ │ ├── fixerio_integration_test.go │ │ │ └── fixerio_test.go │ │ ├── flatio/ │ │ │ ├── flatio.go │ │ │ ├── flatio_integration_test.go │ │ │ └── flatio_test.go │ │ ├── fleetbase/ │ │ │ ├── fleetbase.go │ │ │ ├── fleetbase_integration_test.go │ │ │ └── fleetbase_test.go │ │ ├── flexport/ │ │ │ ├── flexport.go │ │ │ └── flexport_test.go │ │ ├── flickr/ │ │ │ ├── flickr.go │ │ │ ├── flickr_integration_test.go │ │ │ └── flickr_test.go │ │ ├── flightapi/ │ │ │ ├── flightapi.go │ │ │ ├── flightapi_integration_test.go │ │ │ └── flightapi_test.go │ │ ├── flightlabs/ │ │ │ ├── flightlabs.go │ │ │ ├── flightlabs_integration_test.go │ │ │ └── flightlabs_test.go │ │ ├── flightstats/ │ │ │ ├── flightstats.go │ │ │ ├── flightstats_integration_test.go │ │ │ └── flightstats_test.go │ │ ├── float/ │ │ │ ├── float.go │ │ │ ├── float_integration_test.go │ │ │ └── float_test.go │ │ ├── flowflu/ │ │ │ ├── flowflu.go │ │ │ ├── flowflu_integration_test.go │ │ │ └── flowflu_test.go │ │ ├── flutterwave/ │ │ │ ├── flutterwave.go │ │ │ ├── flutterwave_integration_test.go │ │ │ └── flutterwave_test.go │ │ ├── flyio/ │ │ │ ├── flyio.go │ │ │ ├── flyio_integration_test.go │ │ │ └── flyio_test.go │ │ ├── fmfw/ │ │ │ ├── fmfw.go │ │ │ ├── fmfw_integration_test.go │ │ │ └── fmfw_test.go │ │ ├── formbucket/ │ │ │ ├── formbucket.go │ │ │ ├── formbucket_integration_test.go │ │ │ └── formbucket_test.go │ │ ├── formcraft/ │ │ │ ├── formcraft.go │ │ │ ├── formcraft_integration_test.go │ │ │ └── formcraft_test.go │ │ ├── formio/ │ │ │ ├── formio.go │ │ │ ├── formio_integration_test.go │ │ │ └── formio_test.go │ │ ├── formsite/ │ │ │ ├── formsite.go │ │ │ ├── formsite_integration_test.go │ │ │ └── formsite_test.go │ │ ├── foursquare/ │ │ │ ├── foursquare.go │ │ │ ├── foursquare_integration_test.go │ │ │ └── foursquare_test.go │ │ ├── fp_badlist.txt │ │ ├── fp_programmingbooks.txt │ │ ├── fp_uuids.txt │ │ ├── fp_words.txt │ │ ├── frameio/ │ │ │ ├── frameio.go │ │ │ ├── frameio_integration_test.go │ │ │ └── frameio_test.go │ │ ├── freshbooks/ │ │ │ ├── freshbooks.go │ │ │ ├── freshbooks_integration_test.go │ │ │ └── freshbooks_test.go │ │ ├── freshdesk/ │ │ │ ├── freshdesk.go │ │ │ ├── freshdesk_integration_test.go │ │ │ └── freshdesk_test.go │ │ ├── front/ │ │ │ ├── front.go │ │ │ ├── front_integration_test.go │ │ │ └── front_test.go │ │ ├── ftp/ │ │ │ ├── ftp.go │ │ │ ├── ftp_integration_test.go │ │ │ └── ftp_test.go │ │ ├── fulcrum/ │ │ │ ├── fulcrum.go │ │ │ ├── fulcrum_integration_test.go │ │ │ └── fulcrum_test.go │ │ ├── fullstory/ │ │ │ ├── v1/ │ │ │ │ ├── fullstory.go │ │ │ │ ├── fullstory_integration_test.go │ │ │ │ └── fullstory_test.go │ │ │ └── v2/ │ │ │ ├── fullstory_integration_test.go │ │ │ ├── fullstory_v2.go │ │ │ └── fullstory_v2_test.go │ │ ├── fxmarket/ │ │ │ ├── fxmarket.go │ │ │ ├── fxmarket_integration_test.go │ │ │ └── fxmarket_test.go │ │ ├── gcp/ │ │ │ ├── gcp.go │ │ │ ├── gcp_integration_test.go │ │ │ └── gcp_test.go │ │ ├── gcpapplicationdefaultcredentials/ │ │ │ ├── gcpapplicationdefaultcredentials.go │ │ │ ├── gcpapplicationdefaultcredentials_integration_test.go │ │ │ └── gcpapplicationdefaultcredentials_test.go │ │ ├── geckoboard/ │ │ │ ├── geckoboard.go │ │ │ ├── geckoboard_integration_test.go │ │ │ └── geckoboard_test.go │ │ ├── gemini/ │ │ │ ├── gemini.go │ │ │ ├── gemini_integration_test.go │ │ │ └── gemini_test.go │ │ ├── generic/ │ │ │ ├── generic.go │ │ │ ├── generic_integration_test.go │ │ │ └── generic_test.go │ │ ├── gengo/ │ │ │ ├── gengo.go │ │ │ ├── gengo_integration_test.go │ │ │ └── gengo_test.go │ │ ├── geoapify/ │ │ │ ├── geoapify.go │ │ │ ├── geoapify_integration_test.go │ │ │ └── geoapify_test.go │ │ ├── geocode/ │ │ │ ├── geocode.go │ │ │ ├── geocode_integration_test.go │ │ │ └── geocode_test.go │ │ ├── geocodify/ │ │ │ ├── geocodify.go │ │ │ ├── geocodify_integration_test.go │ │ │ └── geocodify_test.go │ │ ├── geocodio/ │ │ │ ├── geocodio.go │ │ │ ├── geocodio_integration_test.go │ │ │ └── geocodio_test.go │ │ ├── geoipifi/ │ │ │ ├── geoipifi.go │ │ │ ├── geoipifi_integration_test.go │ │ │ └── geoipifi_test.go │ │ ├── getemail/ │ │ │ ├── getemail.go │ │ │ ├── getemail_integration_test.go │ │ │ └── getemail_test.go │ │ ├── getemails/ │ │ │ ├── getemails.go │ │ │ ├── getemails_integration_test.go │ │ │ └── getemails_test.go │ │ ├── getgeoapi/ │ │ │ ├── getgeoapi.go │ │ │ ├── getgeoapi_integration_test.go │ │ │ └── getgeoapi_test.go │ │ ├── getgist/ │ │ │ ├── getgist.go │ │ │ ├── getgist_integration_test.go │ │ │ └── getgist_test.go │ │ ├── getresponse/ │ │ │ ├── getresponse.go │ │ │ ├── getresponse_integration_test.go │ │ │ └── getresponse_test.go │ │ ├── getsandbox/ │ │ │ ├── getsandbox.go │ │ │ ├── getsandbox_integration_test.go │ │ │ └── getsandbox_test.go │ │ ├── github/ │ │ │ ├── v1/ │ │ │ │ ├── github_integration_test.go │ │ │ │ ├── github_old.go │ │ │ │ └── github_old_test.go │ │ │ └── v2/ │ │ │ ├── github.go │ │ │ ├── github_integration_test.go │ │ │ └── github_test.go │ │ ├── github_oauth2/ │ │ │ ├── github_oauth2.go │ │ │ └── github_oauth2_test.go │ │ ├── githubapp/ │ │ │ ├── githubapp.go │ │ │ ├── githubapp_integration_test.go │ │ │ └── githubapp_test.go │ │ ├── gitlab/ │ │ │ ├── v1/ │ │ │ │ ├── gitlab.go │ │ │ │ ├── gitlab_integration_test.go │ │ │ │ └── gitlab_v1_test.go │ │ │ ├── v2/ │ │ │ │ ├── gitlab_integration_test.go │ │ │ │ ├── gitlab_v2.go │ │ │ │ └── gitlab_v2_test.go │ │ │ └── v3/ │ │ │ ├── gitlab_v3.go │ │ │ ├── gitlab_v3_integration_test.go │ │ │ └── gitlab_v3_test.go │ │ ├── gitter/ │ │ │ ├── gitter.go │ │ │ ├── gitter_integration_test.go │ │ │ └── gitter_test.go │ │ ├── glassnode/ │ │ │ ├── glassnode.go │ │ │ ├── glassnode_integration_test.go │ │ │ └── glassnode_test.go │ │ ├── gocanvas/ │ │ │ ├── gocanvas.go │ │ │ ├── gocanvas_integration_test.go │ │ │ └── gocanvas_test.go │ │ ├── gocardless/ │ │ │ ├── gocardless.go │ │ │ ├── gocardless_integration_test.go │ │ │ └── gocardless_test.go │ │ ├── godaddy/ │ │ │ ├── v1/ │ │ │ │ ├── godaddy.go │ │ │ │ ├── godaddy_integration_test.go │ │ │ │ └── godaddy_test.go │ │ │ └── v2/ │ │ │ ├── godaddy.go │ │ │ ├── godaddy_integration_test.go │ │ │ └── godaddy_test.go │ │ ├── goodday/ │ │ │ ├── goodday.go │ │ │ ├── goodday_integration_test.go │ │ │ └── goodday_test.go │ │ ├── googlegemini/ │ │ │ ├── googlegemini.go │ │ │ ├── googlegemini_integration_test.go │ │ │ └── googlegemini_test.go │ │ ├── googleoauth2/ │ │ │ ├── googleoauth2_access_token.go │ │ │ ├── googleoauth2_access_token_test.go │ │ │ └── googleoauth2_integration_test.go │ │ ├── grafana/ │ │ │ ├── grafana.go │ │ │ ├── grafana_integration_test.go │ │ │ └── grafana_test.go │ │ ├── grafanaserviceaccount/ │ │ │ ├── grafanaserviceaccount.go │ │ │ ├── grafanaserviceaccount_integration_test.go │ │ │ └── grafanaserviceaccount_test.go │ │ ├── graphcms/ │ │ │ ├── graphcms.go │ │ │ ├── graphcms_integration_test.go │ │ │ └── graphcms_test.go │ │ ├── graphhopper/ │ │ │ ├── graphhopper.go │ │ │ ├── graphhopper_integration_test.go │ │ │ └── graphhopper_test.go │ │ ├── groovehq/ │ │ │ ├── groovehq.go │ │ │ ├── groovehq_integration_test.go │ │ │ └── groovehq_test.go │ │ ├── groq/ │ │ │ ├── groq.go │ │ │ ├── groq_integration_test.go │ │ │ └── groq_test.go │ │ ├── gtmetrix/ │ │ │ ├── gtmetrix.go │ │ │ ├── gtmetrix_integration_test.go │ │ │ └── gtmetrix_test.go │ │ ├── guardianapi/ │ │ │ ├── guardianapi.go │ │ │ ├── guardianapi_integration_test.go │ │ │ └── guardianapi_test.go │ │ ├── gumroad/ │ │ │ ├── gumroad.go │ │ │ ├── gumroad_integration_test.go │ │ │ └── gumroad_test.go │ │ ├── guru/ │ │ │ ├── guru.go │ │ │ ├── guru_integration_test.go │ │ │ └── guru_test.go │ │ ├── gyazo/ │ │ │ ├── gyazo.go │ │ │ ├── gyazo_integration_test.go │ │ │ └── gyazo_test.go │ │ ├── happyscribe/ │ │ │ ├── happyscribe.go │ │ │ ├── happyscribe_integration_test.go │ │ │ └── happyscribe_test.go │ │ ├── harness/ │ │ │ ├── harness.go │ │ │ ├── harness_integration_test.go │ │ │ └── harness_test.go │ │ ├── harvest/ │ │ │ ├── harvest.go │ │ │ ├── harvest_integration_test.go │ │ │ └── harvest_test.go │ │ ├── hashicorpvaultauth/ │ │ │ ├── hashicorpvaultauth.go │ │ │ ├── hashicorpvaultauth_integration_test.go │ │ │ └── hashicorpvaultauth_test.go │ │ ├── hasura/ │ │ │ ├── hasura.go │ │ │ ├── hasura_integration_test.go │ │ │ └── hasura_test.go │ │ ├── hellosign/ │ │ │ ├── hellosign.go │ │ │ ├── hellosign_integration_test.go │ │ │ └── hellosign_test.go │ │ ├── helpcrunch/ │ │ │ ├── helpcrunch.go │ │ │ ├── helpcrunch_integration_test.go │ │ │ └── helpcrunch_test.go │ │ ├── helpscout/ │ │ │ ├── helpscout.go │ │ │ ├── helpscout_integration_test.go │ │ │ └── helpscout_test.go │ │ ├── hereapi/ │ │ │ ├── hereapi.go │ │ │ ├── hereapi_integration_test.go │ │ │ └── hereapi_test.go │ │ ├── heroku/ │ │ │ ├── v1/ │ │ │ │ ├── heroku.go │ │ │ │ ├── heroku_integration_test.go │ │ │ │ └── heroku_test.go │ │ │ └── v2/ │ │ │ ├── heroku.go │ │ │ ├── heroku_integration_test.go │ │ │ └── heroku_test.go │ │ ├── hive/ │ │ │ ├── hive.go │ │ │ ├── hive_integration_test.go │ │ │ └── hive_test.go │ │ ├── hiveage/ │ │ │ ├── hiveage.go │ │ │ ├── hiveage_integration_test.go │ │ │ └── hiveage_test.go │ │ ├── holidayapi/ │ │ │ ├── holidayapi.go │ │ │ ├── holidayapi_integration_test.go │ │ │ └── holidayapi_test.go │ │ ├── holistic/ │ │ │ ├── holistic.go │ │ │ ├── holistic_integration_test.go │ │ │ └── holistic_test.go │ │ ├── honeycomb/ │ │ │ ├── honeycomb.go │ │ │ ├── honeycomb_integration_test.go │ │ │ └── honeycomb_test.go │ │ ├── host/ │ │ │ ├── host.go │ │ │ ├── host_integration_test.go │ │ │ └── host_test.go │ │ ├── html2pdf/ │ │ │ ├── html2pdf.go │ │ │ ├── html2pdf_integration_test.go │ │ │ └── html2pdf_test.go │ │ ├── http.go │ │ ├── http_test.go │ │ ├── hubspot_apikey/ │ │ │ ├── v1/ │ │ │ │ ├── apikey.go │ │ │ │ ├── apikey_integration_test.go │ │ │ │ └── apikey_test.go │ │ │ └── v2/ │ │ │ ├── apikey.go │ │ │ ├── apikey_integration_test.go │ │ │ └── apikey_test.go │ │ ├── huggingface/ │ │ │ ├── huggingface.go │ │ │ ├── huggingface_integration_test.go │ │ │ └── huggingface_test.go │ │ ├── humanity/ │ │ │ ├── humanity.go │ │ │ ├── humanity_integration_test.go │ │ │ └── humanity_test.go │ │ ├── hunter/ │ │ │ ├── hunter.go │ │ │ ├── hunter_integration_test.go │ │ │ └── hunter_test.go │ │ ├── hybiscus/ │ │ │ ├── hybiscus.go │ │ │ ├── hybiscus_integration_test.go │ │ │ └── hybiscus_test.go │ │ ├── hypertrack/ │ │ │ ├── hypertrack.go │ │ │ ├── hypertrack_integration_test.go │ │ │ └── hypertrack_test.go │ │ ├── ibmclouduserkey/ │ │ │ ├── ibmclouduserkey.go │ │ │ ├── ibmclouduserkey_integration_test.go │ │ │ └── ibmclouduserkey_test.go │ │ ├── iconfinder/ │ │ │ ├── iconfinder.go │ │ │ ├── iconfinder_integreation_test.go │ │ │ └── iconfinder_test.go │ │ ├── iexapis/ │ │ │ ├── iexapis.go │ │ │ ├── iexapis_integration_test.go │ │ │ └── iexapis_test.go │ │ ├── iexcloud/ │ │ │ ├── iexcloud.go │ │ │ ├── iexcloud_integration_test.go │ │ │ └── iexcloud_test.go │ │ ├── imagekit/ │ │ │ ├── imagekit.go │ │ │ ├── imagekit_integration_test.go │ │ │ └── imagekit_test.go │ │ ├── imagga/ │ │ │ ├── imagga.go │ │ │ ├── imagga_integration_test.go │ │ │ └── imagga_test.go │ │ ├── impala/ │ │ │ ├── impala.go │ │ │ ├── impala_integration_test.go │ │ │ └── impala_test.go │ │ ├── infura/ │ │ │ ├── infura.go │ │ │ ├── infura_integration_test.go │ │ │ └── infura_test.go │ │ ├── insightly/ │ │ │ ├── insightly.go │ │ │ ├── insightly_integration_test.go │ │ │ └── insightly_test.go │ │ ├── instabot/ │ │ │ ├── instabot.go │ │ │ ├── instabot_integration_test.go │ │ │ └── instabot_test.go │ │ ├── instamojo/ │ │ │ ├── instamojo.go │ │ │ ├── instamojo_integration_test.go │ │ │ └── instamojo_test.go │ │ ├── intercom/ │ │ │ ├── intercom.go │ │ │ ├── intercom_integration_test.go │ │ │ └── intercom_test.go │ │ ├── interseller/ │ │ │ ├── interseller.go │ │ │ ├── interseller_integration_test.go │ │ │ └── interseller_test.go │ │ ├── intra42/ │ │ │ ├── intra42.go │ │ │ ├── intra42_integration_test.go │ │ │ └── intra42_test.go │ │ ├── intrinio/ │ │ │ ├── intrinio.go │ │ │ ├── intrinio_integration_test.go │ │ │ └── intrinio_test.go │ │ ├── invoiceocean/ │ │ │ ├── invoiceocean.go │ │ │ ├── invoiceocean_integration_test.go │ │ │ └── invoiceocean_test.go │ │ ├── ip2location/ │ │ │ ├── ip2location.go │ │ │ ├── ip2location_integration_test.go │ │ │ └── ip2location_test.go │ │ ├── ipapi/ │ │ │ ├── ipapi.go │ │ │ ├── ipapi_integration_test.go │ │ │ └── ipapi_test.go │ │ ├── ipgeolocation/ │ │ │ ├── ipgeolocation.go │ │ │ ├── ipgeolocation_integration_test.go │ │ │ └── ipgeolocation_test.go │ │ ├── ipinfo/ │ │ │ ├── ipinfo.go │ │ │ ├── ipinfo_integration_test.go │ │ │ └── ipinfo_test.go │ │ ├── ipinfodb/ │ │ │ ├── ipinfodb.go │ │ │ ├── ipinfodb_integration_test.go │ │ │ └── ipinfodb_test.go │ │ ├── ipquality/ │ │ │ ├── ipquality.go │ │ │ ├── ipquality_integration_test.go │ │ │ └── ipquality_test.go │ │ ├── ipstack/ │ │ │ ├── ipstack.go │ │ │ ├── ipstack_integration_test.go │ │ │ └── ipstack_test.go │ │ ├── jdbc/ │ │ │ ├── jdbc.go │ │ │ ├── jdbc_integration_test.go │ │ │ ├── jdbc_test.go │ │ │ ├── models.go │ │ │ ├── mysql.go │ │ │ ├── mysql_integration_test.go │ │ │ ├── mysql_test.go │ │ │ ├── postgres.go │ │ │ ├── postgres_integration_test.go │ │ │ ├── postgres_test.go │ │ │ ├── sqlserver.go │ │ │ ├── sqlserver_integration_test.go │ │ │ └── sqlserver_test.go │ │ ├── jiratoken/ │ │ │ ├── v1/ │ │ │ │ ├── jiratoken.go │ │ │ │ ├── jiratoken_integration_test.go │ │ │ │ └── jiratoken_test.go │ │ │ └── v2/ │ │ │ ├── jiratoken_v2.go │ │ │ ├── jiratoken_v2_integration_test.go │ │ │ └── jiratoken_v2_test.go │ │ ├── jotform/ │ │ │ ├── jotform.go │ │ │ ├── jotform_integration_test.go │ │ │ └── jotform_test.go │ │ ├── jumpcloud/ │ │ │ ├── jumpcloud.go │ │ │ ├── jumpcloud_integration_test.go │ │ │ └── jumpcloud_test.go │ │ ├── jupiterone/ │ │ │ ├── jupiterone.go │ │ │ ├── jupiterone_integration_test.go │ │ │ └── jupiterone_test.go │ │ ├── juro/ │ │ │ ├── juro.go │ │ │ ├── juro_integration_test.go │ │ │ └── juro_test.go │ │ ├── jwt/ │ │ │ ├── jwt.go │ │ │ └── jwt_test.go │ │ ├── kanban/ │ │ │ ├── kanban.go │ │ │ ├── kanban_integration_test.go │ │ │ └── kanban_test.go │ │ ├── kanbantool/ │ │ │ ├── kanbantool.go │ │ │ ├── kanbantool_integration_test.go │ │ │ └── kanbantool_test.go │ │ ├── karmacrm/ │ │ │ ├── karmacrm.go │ │ │ ├── karmacrm_integration_test.go │ │ │ └── karmacrm_test.go │ │ ├── keenio/ │ │ │ ├── keenio.go │ │ │ ├── keenio_integration_test.go │ │ │ └── keenio_test.go │ │ ├── kickbox/ │ │ │ ├── kickbox.go │ │ │ ├── kickbox_integration_test.go │ │ │ └── kickbox_test.go │ │ ├── klaviyo/ │ │ │ ├── klaviyo.go │ │ │ ├── klaviyo_integration_test.go │ │ │ └── klaviyo_test.go │ │ ├── klipfolio/ │ │ │ ├── klipfolio.go │ │ │ ├── klipfolio_integration_test.go │ │ │ └── klipfolio_test.go │ │ ├── knapsackpro/ │ │ │ ├── knapsackpro.go │ │ │ ├── knapsackpro_integration_test.go │ │ │ └── knapsackpro_test.go │ │ ├── kontent/ │ │ │ ├── kontent.go │ │ │ ├── kontent_integration_test.go │ │ │ └── kontent_test.go │ │ ├── kraken/ │ │ │ ├── kraken.go │ │ │ ├── kraken_integration_test.go │ │ │ └── kraken_test.go │ │ ├── kucoin/ │ │ │ ├── kucoin.go │ │ │ ├── kucoin_integration_test.go │ │ │ └── kucoin_test.go │ │ ├── kylas/ │ │ │ ├── kylas.go │ │ │ ├── kylas_integration_test.go │ │ │ └── kylas_test.go │ │ ├── langfuse/ │ │ │ ├── langfuse.go │ │ │ ├── langfuse_integration_test.go │ │ │ └── langfuse_test.go │ │ ├── langsmith/ │ │ │ ├── langsmith.go │ │ │ ├── langsmith_integration_test.go │ │ │ └── langsmith_test.go │ │ ├── languagelayer/ │ │ │ ├── languagelayer.go │ │ │ ├── languagelayer_integration_test.go │ │ │ └── languagelayer_test.go │ │ ├── larksuite/ │ │ │ ├── larksuite.go │ │ │ ├── larksuite_integration_test.go │ │ │ └── larksuite_test.go │ │ ├── larksuiteapikey/ │ │ │ ├── larksuiteapikey.go │ │ │ ├── larksuiteapikey_integration_test.go │ │ │ └── larksuiteapikey_test.go │ │ ├── launchdarkly/ │ │ │ ├── launchdarkly.go │ │ │ ├── launchdarkly_integration_test.go │ │ │ └── launchdarkly_test.go │ │ ├── ldap/ │ │ │ ├── ldap.go │ │ │ ├── ldap_integration_test.go │ │ │ └── ldap_test.go │ │ ├── leadfeeder/ │ │ │ ├── leadfeeder.go │ │ │ ├── leadfeeder_integration_test.go │ │ │ └── leadfeeder_test.go │ │ ├── lemlist/ │ │ │ ├── lemlist.go │ │ │ ├── lemlist_integration_test.go │ │ │ └── lemlist_test.go │ │ ├── lemonsqueezy/ │ │ │ ├── lemonsqueezy.go │ │ │ ├── lemonsqueezy_integration_test.go │ │ │ └── lemonsqueezy_test.go │ │ ├── lendflow/ │ │ │ ├── lendflow.go │ │ │ ├── lendflow_integration_test.go │ │ │ └── lendflow_test.go │ │ ├── lessannoyingcrm/ │ │ │ ├── lessannoyingcrm.go │ │ │ ├── lessannoyingcrm_integration_test.go │ │ │ └── lessannoyingcrm_test.go │ │ ├── lexigram/ │ │ │ ├── lexigram.go │ │ │ ├── lexigram_integration_test.go │ │ │ └── lexigram_test.go │ │ ├── linearapi/ │ │ │ ├── linearapi.go │ │ │ ├── linearapi_integration_test.go │ │ │ └── linearapi_test.go │ │ ├── linemessaging/ │ │ │ ├── linemessaging.go │ │ │ ├── linemessaging_integration_test.go │ │ │ └── linemessaging_test.go │ │ ├── linenotify/ │ │ │ ├── linenotify.go │ │ │ ├── linenotify_integration_test.go │ │ │ └── linenotify_test.go │ │ ├── linkpreview/ │ │ │ ├── linkpreview.go │ │ │ ├── linkpreview_integration_test.go │ │ │ └── linkpreview_test.go │ │ ├── liveagent/ │ │ │ ├── liveagent.go │ │ │ ├── liveagent_integration_test.go │ │ │ └── liveagent_test.go │ │ ├── livestorm/ │ │ │ ├── livestorm.go │ │ │ ├── livestorm_integration_test.go │ │ │ └── livestorm_test.go │ │ ├── loadmill/ │ │ │ ├── loadmill.go │ │ │ ├── loadmill_integration_test.go │ │ │ └── loadmill_test.go │ │ ├── lob/ │ │ │ ├── lob.go │ │ │ ├── lob_integration_test.go │ │ │ └── lob_test.go │ │ ├── locationiq/ │ │ │ ├── locationiq.go │ │ │ ├── locationiq_integration_test.go │ │ │ └── locationiq_test.go │ │ ├── loggly/ │ │ │ ├── loggly.go │ │ │ ├── loggly_integration_test.go │ │ │ └── loggly_test.go │ │ ├── loginradius/ │ │ │ ├── loginradius.go │ │ │ ├── loginradius_integration_test.go │ │ │ └── loginradius_test.go │ │ ├── logzio/ │ │ │ ├── logzio.go │ │ │ ├── logzio_integration_test.go │ │ │ └── logzio_test.go │ │ ├── lokalisetoken/ │ │ │ ├── lokalisetoken.go │ │ │ ├── lokalisetoken_integration_test.go │ │ │ └── lokalisetoken_test.go │ │ ├── loyverse/ │ │ │ ├── loyverse.go │ │ │ ├── loyverse_integration_test.go │ │ │ └── loyverse_test.go │ │ ├── lunchmoney/ │ │ │ ├── lunchmoney.go │ │ │ ├── lunchmoney_integration_test.go │ │ │ └── lunchmoney_test.go │ │ ├── luno/ │ │ │ ├── luno.go │ │ │ ├── luno_integration_test.go │ │ │ └── luno_test.go │ │ ├── m3o/ │ │ │ ├── m3o.go │ │ │ ├── m3o_integration_test.go │ │ │ └── m3o_test.go │ │ ├── madkudu/ │ │ │ ├── madkudu.go │ │ │ ├── madkudu_integration_test.go │ │ │ └── madkudu_test.go │ │ ├── magicbell/ │ │ │ ├── magicbell.go │ │ │ ├── magicbell_integration_test.go │ │ │ └── magicbell_test.go │ │ ├── magnetic/ │ │ │ ├── magnetic.go │ │ │ ├── magnetic_integration_test.go │ │ │ └── magnetic_test.go │ │ ├── mailboxlayer/ │ │ │ ├── mailboxlayer.go │ │ │ ├── mailboxlayer_integration_test.go │ │ │ └── mailboxlayer_test.go │ │ ├── mailchimp/ │ │ │ ├── mailchimp.go │ │ │ ├── mailchimp_integration_test.go │ │ │ └── mailchimp_test.go │ │ ├── mailerlite/ │ │ │ ├── mailerlite.go │ │ │ ├── mailerlite_integration_test.go │ │ │ └── mailerlite_test.go │ │ ├── mailgun/ │ │ │ ├── mailgun.go │ │ │ ├── mailgun_integration_test.go │ │ │ └── mailgun_test.go │ │ ├── mailjetbasicauth/ │ │ │ ├── mailjetbasicauth.go │ │ │ ├── mailjetbasicauth_integration_test.go │ │ │ └── mailjetbasicauth_test.go │ │ ├── mailjetsms/ │ │ │ ├── mailjetsms.go │ │ │ ├── mailjetsms_integration_test.go │ │ │ └── mailjetsms_test.go │ │ ├── mailmodo/ │ │ │ ├── mailmodo.go │ │ │ ├── mailmodo_integration_test.go │ │ │ └── mailmodo_test.go │ │ ├── mailsac/ │ │ │ ├── mailsac.go │ │ │ ├── mailsac_integration_test.go │ │ │ └── mailsac_test.go │ │ ├── mandrill/ │ │ │ ├── mandrill.go │ │ │ ├── mandrill_integration_test.go │ │ │ └── mandrill_test.go │ │ ├── manifest/ │ │ │ ├── manifest.go │ │ │ ├── manifest_integration_test.go │ │ │ └── manifest_test.go │ │ ├── mapbox/ │ │ │ ├── mapbox.go │ │ │ ├── mapbox_integration_test.go │ │ │ └── mapbox_test.go │ │ ├── mapquest/ │ │ │ ├── mapquest.go │ │ │ ├── mapquest_integration_test.go │ │ │ └── mapquest_test.go │ │ ├── marketstack/ │ │ │ ├── marketstack.go │ │ │ ├── marketstack_integration_test.go │ │ │ └── marketstack_test.go │ │ ├── mattermostpersonaltoken/ │ │ │ ├── mattermostpersonaltoken.go │ │ │ ├── mattermostpersonaltoken_integration_test.go │ │ │ └── mattermostpersonaltoken_test.go │ │ ├── mavenlink/ │ │ │ ├── mavenlink.go │ │ │ ├── mavenlink_integration_test.go │ │ │ └── mavenlink_test.go │ │ ├── maxmindlicense/ │ │ │ ├── v1/ │ │ │ │ ├── maxmindlicense.go │ │ │ │ ├── maxmindlicense_integration_test.go │ │ │ │ └── maxmindlicense_test.go │ │ │ └── v2/ │ │ │ ├── maxmindlicense_v2.go │ │ │ ├── maxmindlicense_v2_integration_test.go │ │ │ └── maxmindlicense_v2_test.go │ │ ├── meaningcloud/ │ │ │ ├── meaningcloud.go │ │ │ ├── meaningcloud_integration_test.go │ │ │ └── meaningcloud_test.go │ │ ├── mediastack/ │ │ │ ├── mediastack.go │ │ │ ├── mediastack_integration_test.go │ │ │ └── mediastack_test.go │ │ ├── meistertask/ │ │ │ ├── meistertask.go │ │ │ ├── meistertask_integration_test.go │ │ │ └── meistertask_test.go │ │ ├── meraki/ │ │ │ ├── meraki.go │ │ │ ├── meraki_integration_test.go │ │ │ └── meraki_test.go │ │ ├── mesibo/ │ │ │ ├── mesibo.go │ │ │ ├── mesibo_integration_test.go │ │ │ └── mesibo_test.go │ │ ├── messagebird/ │ │ │ ├── messagebird.go │ │ │ ├── messagebird_integration_test.go │ │ │ └── messagebird_test.go │ │ ├── metaapi/ │ │ │ ├── metaapi.go │ │ │ ├── metaapi_integration_test.go │ │ │ └── metaapi_test.go │ │ ├── metabase/ │ │ │ ├── metabase.go │ │ │ ├── metabase_integration_test.go │ │ │ └── metabase_test.go │ │ ├── metrilo/ │ │ │ ├── metrilo.go │ │ │ ├── metrilo_integration_test.go │ │ │ └── metrilo_test.go │ │ ├── microsoftteamswebhook/ │ │ │ ├── microsoftteamswebhook.go │ │ │ ├── microsoftteamswebhook_integration_test.go │ │ │ └── microsoftteamswebhook_test.go │ │ ├── mindmeister/ │ │ │ ├── mindmeister.go │ │ │ ├── mindmeister_integration_test.go │ │ │ └── mindmeister_test.go │ │ ├── miro/ │ │ │ ├── miro.go │ │ │ ├── miro_integration_test.go │ │ │ └── miro_test.go │ │ ├── mite/ │ │ │ ├── mite.go │ │ │ ├── mite_integration_test.go │ │ │ └── mite_test.go │ │ ├── mixmax/ │ │ │ ├── mixmax.go │ │ │ ├── mixmax_integration_test.go │ │ │ └── mixmax_test.go │ │ ├── mixpanel/ │ │ │ ├── mixpanel.go │ │ │ ├── mixpanel_integration_test.go │ │ │ └── mixpanel_test.go │ │ ├── mockaroo/ │ │ │ ├── mockaroo.go │ │ │ ├── mockaroo_integration_test.go │ │ │ └── mockaroo_test.go │ │ ├── moderation/ │ │ │ ├── moderation.go │ │ │ ├── moderation_integration_test.go │ │ │ └── moderation_test.go │ │ ├── monday/ │ │ │ ├── monday.go │ │ │ ├── monday_integration_test.go │ │ │ └── monday_test.go │ │ ├── mongodb/ │ │ │ ├── mongodb.go │ │ │ ├── mongodb_integration_test.go │ │ │ └── mongodb_test.go │ │ ├── monkeylearn/ │ │ │ ├── monkeylearn.go │ │ │ ├── monkeylearn_integration_test.go │ │ │ └── monkeylearn_test.go │ │ ├── moonclerk/ │ │ │ ├── moonclerk.go │ │ │ ├── moonclerk_integration_test.go │ │ │ └── moonclerk_test.go │ │ ├── moosend/ │ │ │ ├── moosend.go │ │ │ ├── moosend_integration_test.go │ │ │ └── moosend_test.go │ │ ├── moralis/ │ │ │ ├── moralis.go │ │ │ ├── moralis_integration_test.go │ │ │ └── moralis_test.go │ │ ├── mrticktock/ │ │ │ ├── mrticktock.go │ │ │ ├── mrticktock_test.go │ │ │ └── mrticktok_integration_test.go │ │ ├── multi_part_credential_provider.go │ │ ├── multi_part_credential_provider_test.go │ │ ├── mux/ │ │ │ ├── mux.go │ │ │ ├── mux_integration_test.go │ │ │ └── mux_test.go │ │ ├── myfreshworks/ │ │ │ ├── myfreshworks.go │ │ │ ├── myfreshworks_integration_test.go │ │ │ └── myfreshworks_test.go │ │ ├── myintervals/ │ │ │ ├── myintervals.go │ │ │ ├── myintervals_integration_test.go │ │ │ └── myintervals_test.go │ │ ├── nasdaqdatalink/ │ │ │ ├── nasdaqdatalink.go │ │ │ ├── nasdaqdatalink_integration_test.go │ │ │ └── nasdaqdatalink_test.go │ │ ├── nethunt/ │ │ │ ├── nethunt.go │ │ │ ├── nethunt_integration_test.go │ │ │ └── nethunt_test.go │ │ ├── netlify/ │ │ │ ├── v1/ │ │ │ │ ├── netlify_v1.go │ │ │ │ ├── netlify_v1_integration_test.go │ │ │ │ └── netlify_v1_test.go │ │ │ └── v2/ │ │ │ ├── netlify_v2.go │ │ │ ├── netlify_v2_integration_test.go │ │ │ └── netlify_v2_test.go │ │ ├── netsuite/ │ │ │ ├── netsuite.go │ │ │ ├── netsuite_integration_test.go │ │ │ └── netsuite_test.go │ │ ├── neutrinoapi/ │ │ │ ├── neutrinoapi.go │ │ │ ├── neutrinoapi_integration_test.go │ │ │ └── neutrinoapi_test.go │ │ ├── newrelicpersonalapikey/ │ │ │ ├── newrelicpersonalapikey.go │ │ │ ├── newrelicpersonalapikey_integration_test.go │ │ │ └── newrelicpersonalapikey_test.go │ │ ├── newsapi/ │ │ │ ├── newsapi.go │ │ │ ├── newsapi_integration_test.go │ │ │ └── newsapi_test.go │ │ ├── newscatcher/ │ │ │ ├── newscatcher.go │ │ │ ├── newscatcher_integration_test.go │ │ │ └── newscatcher_test.go │ │ ├── nexmoapikey/ │ │ │ ├── nexmoapikey.go │ │ │ ├── nexmoapikey_integration_test.go │ │ │ └── nexmoapikey_test.go │ │ ├── nftport/ │ │ │ ├── nftport.go │ │ │ ├── nftport_integration_test.go │ │ │ └── nftport_test.go │ │ ├── ngc/ │ │ │ ├── ngc.go │ │ │ ├── ngc_integration_test.go │ │ │ └── ngc_test.go │ │ ├── ngrok/ │ │ │ ├── ngrok.go │ │ │ ├── ngrok_integration_test.go │ │ │ └── ngrok_test.go │ │ ├── nicereply/ │ │ │ ├── nicereply.go │ │ │ ├── nicereply_integration_test.go │ │ │ └── nicereply_test.go │ │ ├── nightfall/ │ │ │ ├── nightfall.go │ │ │ ├── nightfall_integration_test.go │ │ │ └── nightfall_test.go │ │ ├── nimble/ │ │ │ ├── nimble.go │ │ │ ├── nimble_integration_test.go │ │ │ └── nimble_test.go │ │ ├── noticeable/ │ │ │ ├── noticeable.go │ │ │ ├── noticeable_integration_test.go │ │ │ └── noticeable_test.go │ │ ├── notion/ │ │ │ ├── notion.go │ │ │ ├── notion_integration_test.go │ │ │ └── notion_test.go │ │ ├── nozbeteams/ │ │ │ ├── nozbeteams.go │ │ │ ├── nozbeteams_integration_test.go │ │ │ └── nozbeteams_test.go │ │ ├── npmtoken/ │ │ │ ├── npmtoken.go │ │ │ ├── npmtoken_integration_test.go │ │ │ └── npmtoken_test.go │ │ ├── npmtokenv2/ │ │ │ ├── npmtokenv2.go │ │ │ ├── npmtokenv2_integration_test.go │ │ │ └── npmtokenv2_test.go │ │ ├── nugetapikey/ │ │ │ ├── nugetapikey.go │ │ │ ├── nugetapikey_integration_test.go │ │ │ └── nugetapikey_test.go │ │ ├── numverify/ │ │ │ ├── numverify.go │ │ │ ├── numverify_integration_test.go │ │ │ └── numverify_test.go │ │ ├── nutritionix/ │ │ │ ├── nutritionix.go │ │ │ ├── nutritionix_integration_test.go │ │ │ └── nutritionix_test.go │ │ ├── nvapi/ │ │ │ ├── nvapi.go │ │ │ ├── nvapi_integration_test.go │ │ │ └── nvapi_test.go │ │ ├── nylas/ │ │ │ ├── nylas.go │ │ │ ├── nylas_integration_test.go │ │ │ └── nylas_test.go │ │ ├── oanda/ │ │ │ ├── oanda.go │ │ │ ├── oanda_integration_test.go │ │ │ └── oanda_test.go │ │ ├── okta/ │ │ │ ├── okta.go │ │ │ ├── okta_integration_test.go │ │ │ └── okta_test.go │ │ ├── omnisend/ │ │ │ ├── omnisend.go │ │ │ ├── omnisend_integration_test.go │ │ │ └── omnisend_test.go │ │ ├── onedesk/ │ │ │ ├── onedesk.go │ │ │ ├── onedesk_integration_test.go │ │ │ └── onedesk_test.go │ │ ├── onelogin/ │ │ │ ├── onelogin.go │ │ │ ├── onelogin_integration_test.go │ │ │ └── onelogin_test.go │ │ ├── onepagecrm/ │ │ │ ├── onepagecrm.go │ │ │ ├── onepagecrm_integration_test.go │ │ │ └── onepagecrm_test.go │ │ ├── onesignal/ │ │ │ ├── onesignal.go │ │ │ ├── onesignal_integration_test.go │ │ │ └── onesignal_test.go │ │ ├── onfleet/ │ │ │ ├── onfleet.go │ │ │ ├── onfleet_integration_test.go │ │ │ └── onfleet_test.go │ │ ├── oopspam/ │ │ │ ├── oopspam.go │ │ │ ├── oopspam_integration_test.go │ │ │ └── oopspam_test.go │ │ ├── openai/ │ │ │ ├── openai.go │ │ │ ├── openai_integration_test.go │ │ │ └── openai_test.go │ │ ├── openaiadmin/ │ │ │ ├── openaiadmin.go │ │ │ ├── openaiadmin_integration_test.go │ │ │ └── openaiadmin_test.go │ │ ├── opencagedata/ │ │ │ ├── opencagedata.go │ │ │ ├── opencagedata_integration_test.go │ │ │ └── opencagedata_test.go │ │ ├── openuv/ │ │ │ ├── openuv.go │ │ │ ├── openuv_integration_test.go │ │ │ └── openuv_test.go │ │ ├── openvpn/ │ │ │ ├── openvpn.go │ │ │ ├── openvpn_integration_test.go │ │ │ └── openvpn_test.go │ │ ├── openweather/ │ │ │ ├── openweather.go │ │ │ ├── openweather_integration_test.go │ │ │ └── openweather_test.go │ │ ├── opsgenie/ │ │ │ ├── opsgenie.go │ │ │ ├── opsgenie_integration_test.go │ │ │ └── opsgenie_test.go │ │ ├── optimizely/ │ │ │ ├── optimizely.go │ │ │ ├── optimizely_integration_test.go │ │ │ └── optimizely_test.go │ │ ├── overloop/ │ │ │ ├── overloop.go │ │ │ ├── overloop_integration_test.go │ │ │ └── overloop_test.go │ │ ├── owlbot/ │ │ │ ├── owlbot.go │ │ │ ├── owlbot_integration_test.go │ │ │ └── owlbot_test.go │ │ ├── packagecloud/ │ │ │ ├── packagecloud.go │ │ │ ├── packagecloud_integration_test.go │ │ │ └── packagecloud_test.go │ │ ├── pagarme/ │ │ │ ├── pagarme.go │ │ │ ├── pagarme_integration_test.go │ │ │ └── pagarme_test.go │ │ ├── pagerdutyapikey/ │ │ │ ├── pagerdutyapikey.go │ │ │ ├── pagerdutyapikey_integration_test.go │ │ │ └── pagerdutyapikey_test.go │ │ ├── pandadoc/ │ │ │ ├── pandadoc.go │ │ │ ├── pandadoc_integration_test.go │ │ │ └── pandadoc_test.go │ │ ├── pandascore/ │ │ │ ├── pandascore.go │ │ │ ├── pandascore_integration_test.go │ │ │ └── pandascore_test.go │ │ ├── paperform/ │ │ │ ├── paperform.go │ │ │ ├── paperform_integration_test.go │ │ │ └── paperform_test.go │ │ ├── paralleldots/ │ │ │ ├── paralleldots.go │ │ │ ├── paralleldots_integration_test.go │ │ │ └── paralleldots_test.go │ │ ├── parsehub/ │ │ │ ├── parsehub.go │ │ │ ├── parsehub_integration_test.go │ │ │ └── parsehub_test.go │ │ ├── parsers/ │ │ │ ├── parsers.go │ │ │ ├── parsers_integration_test.go │ │ │ └── parsers_test.go │ │ ├── parseur/ │ │ │ ├── parseur.go │ │ │ ├── parseur_integration_test.go │ │ │ └── parseur_test.go │ │ ├── partnerstack/ │ │ │ ├── partnerstack.go │ │ │ ├── partnerstack_integration_test.go │ │ │ └── partnerstack_test.go │ │ ├── pastebin/ │ │ │ ├── pastebin.go │ │ │ ├── pastebin_integration_test.go │ │ │ └── pastebin_test.go │ │ ├── paydirtapp/ │ │ │ ├── paydirtapp.go │ │ │ ├── paydirtapp_integration_test.go │ │ │ └── paydirtapp_test.go │ │ ├── paymoapp/ │ │ │ ├── paymoapp.go │ │ │ ├── paymoapp_integration_test.go │ │ │ └── paymoapp_test.go │ │ ├── paymongo/ │ │ │ ├── paymongo.go │ │ │ ├── paymongo_integration_test.go │ │ │ └── paymongo_test.go │ │ ├── paypaloauth/ │ │ │ ├── paypaloauth.go │ │ │ ├── paypaloauth_integration_test.go │ │ │ └── paypaloauth_test.go │ │ ├── paystack/ │ │ │ ├── paystack.go │ │ │ ├── paystack_integration_test.go │ │ │ └── paystack_test.go │ │ ├── pdflayer/ │ │ │ ├── pdflayer.go │ │ │ ├── pdflayer_integration_test.go │ │ │ └── pdflayer_test.go │ │ ├── pdfshift/ │ │ │ ├── pdfshift.go │ │ │ ├── pdfshift_integration_test.go │ │ │ └── pdfshift_test.go │ │ ├── peopledatalabs/ │ │ │ ├── peopledatalabs.go │ │ │ ├── peopledatalabs_integration_test.go │ │ │ └── peopledatalabs_test.go │ │ ├── pepipost/ │ │ │ ├── pepipost.go │ │ │ ├── pepipost_integration_test.go │ │ │ └── pepipost_test.go │ │ ├── percy/ │ │ │ ├── percy.go │ │ │ ├── percy_integration_test.go │ │ │ └── percy_test.go │ │ ├── photoroom/ │ │ │ ├── photoroom.go │ │ │ ├── photoroom_integration_test.go │ │ │ └── photoroom_test.go │ │ ├── phraseaccesstoken/ │ │ │ ├── phraseaccesstoken.go │ │ │ ├── phraseaccesstoken_integration_test.go │ │ │ └── phraseaccesstoken_test.go │ │ ├── pinata/ │ │ │ ├── pinata.go │ │ │ ├── pinata_integration_test.go │ │ │ └── pinata_test.go │ │ ├── pipedream/ │ │ │ ├── pipedream.go │ │ │ ├── pipedream_integration_test.go │ │ │ └── pipedream_test.go │ │ ├── pipedrive/ │ │ │ ├── pipedrive.go │ │ │ ├── pipedrive_integration_test.go │ │ │ └── pipedrive_test.go │ │ ├── pivotaltracker/ │ │ │ ├── pivotaltracker.go │ │ │ ├── pivotaltracker_integration_test.go │ │ │ └── pivotaltracker_test.go │ │ ├── pixabay/ │ │ │ ├── pixabay.go │ │ │ ├── pixabay_integration_test.go │ │ │ └── pixabay_test.go │ │ ├── plaidkey/ │ │ │ ├── plaidkey.go │ │ │ ├── plaidkey_integration_test.go │ │ │ └── plaidkey_test.go │ │ ├── planetscale/ │ │ │ ├── planetscale.go │ │ │ ├── planetscale_integration_test.go │ │ │ └── planetscale_test.go │ │ ├── planetscaledb/ │ │ │ ├── planetscaledb.go │ │ │ ├── planetscaledb_integration_test.go │ │ │ └── planetscaledb_test.go │ │ ├── planviewleankit/ │ │ │ ├── planviewleankit.go │ │ │ ├── planviewleankit_integration_test.go │ │ │ └── planviewleankit_test.go │ │ ├── planyo/ │ │ │ ├── planyo.go │ │ │ ├── planyo_integration_test.go │ │ │ └── planyo_test.go │ │ ├── plivo/ │ │ │ ├── plivo.go │ │ │ ├── plivo_integration_test.go │ │ │ └── plivo_test.go │ │ ├── podio/ │ │ │ ├── podio.go │ │ │ ├── podio_integration_test.go │ │ │ └── podio_test.go │ │ ├── pollsapi/ │ │ │ ├── pollsapi.go │ │ │ ├── pollsapi_integration_test.go │ │ │ └── pollsapi_test.go │ │ ├── poloniex/ │ │ │ ├── poloniex.go │ │ │ ├── poloniex_integration_test.go │ │ │ └── poloniex_test.go │ │ ├── polygon/ │ │ │ ├── polygon.go │ │ │ ├── polygon_integration_test.go │ │ │ └── polygon_test.go │ │ ├── portainer/ │ │ │ ├── portainer.go │ │ │ ├── portainer_integration_test.go │ │ │ └── portainer_test.go │ │ ├── portainertoken/ │ │ │ ├── portainertoken.go │ │ │ ├── portainertoken_integration_test.go │ │ │ └── portainertoken_test.go │ │ ├── positionstack/ │ │ │ ├── positionstack.go │ │ │ ├── positionstack_integration_test.go │ │ │ └── positionstack_test.go │ │ ├── postageapp/ │ │ │ ├── postageapp.go │ │ │ ├── postageapp_integration_test.go │ │ │ └── postageapp_test.go │ │ ├── postbacks/ │ │ │ ├── postbacks.go │ │ │ ├── postbacks_integration_test.go │ │ │ └── postbacks_test.go │ │ ├── postgres/ │ │ │ ├── postgres.go │ │ │ ├── postgres_integration_test.go │ │ │ └── postgres_test.go │ │ ├── posthog/ │ │ │ ├── posthog.go │ │ │ ├── posthog_integration_test.go │ │ │ └── posthog_test.go │ │ ├── postman/ │ │ │ ├── postman.go │ │ │ ├── postman_integration_test.go │ │ │ └── postman_test.go │ │ ├── postmark/ │ │ │ ├── postmark.go │ │ │ ├── postmark_integration_test.go │ │ │ └── postmark_test.go │ │ ├── powrbot/ │ │ │ ├── powrbot.go │ │ │ ├── powrbot_integration_test.go │ │ │ └── powrbot_test.go │ │ ├── prefect/ │ │ │ ├── prefect.go │ │ │ ├── prefect_integration_test.go │ │ │ └── prefect_test.go │ │ ├── privacy/ │ │ │ ├── privacy.go │ │ │ ├── privacy_integration_test.go │ │ │ └── privacy_test.go │ │ ├── privatekey/ │ │ │ ├── cracker.go │ │ │ ├── cracker_test.go │ │ │ ├── fingerprint.go │ │ │ ├── list.txt │ │ │ ├── normalize.go │ │ │ ├── privatekey.go │ │ │ ├── privatekey_integration_test.go │ │ │ ├── privatekey_test.go │ │ │ ├── ssh_integration.go │ │ │ └── ssh_integration_test.go │ │ ├── prodpad/ │ │ │ ├── prodpad.go │ │ │ ├── prodpad_integration_test.go │ │ │ └── prodpad_test.go │ │ ├── prospectcrm/ │ │ │ ├── prospectcrm.go │ │ │ ├── prospectcrm_integration_test.go │ │ │ └── prospectcrm_test.go │ │ ├── protocolsio/ │ │ │ ├── protocolsio.go │ │ │ ├── protocolsio_integration_test.go │ │ │ └── protocolsio_test.go │ │ ├── proxycrawl/ │ │ │ ├── proxycrawl.go │ │ │ ├── proxycrawl_integration_test.go │ │ │ └── proxycrawl_test.go │ │ ├── pubnubpublishkey/ │ │ │ ├── pubnubpublishkey.go │ │ │ ├── pubnubpublishkey_integration_test.go │ │ │ └── pubnubpublishkey_test.go │ │ ├── pubnubsubscriptionkey/ │ │ │ ├── pubnubsubscriptionkey.go │ │ │ ├── pubnubsubscriptionkey_integration_test.go │ │ │ └── pubnubsubscriptionkey_test.go │ │ ├── pulumi/ │ │ │ ├── pulumi.go │ │ │ ├── pulumi_integration_test.go │ │ │ └── pulumi_test.go │ │ ├── purestake/ │ │ │ ├── purestake.go │ │ │ ├── purestake_integration_test.go │ │ │ └── purestake_test.go │ │ ├── pushbulletapikey/ │ │ │ ├── pushbulletapikey.go │ │ │ ├── pushbulletapikey_integration_test.go │ │ │ └── pushbulletapikey_test.go │ │ ├── pusherchannelkey/ │ │ │ ├── pusherchannelkey.go │ │ │ ├── pusherchannelkey_integration_test.go │ │ │ └── pusherchannelkey_test.go │ │ ├── pypi/ │ │ │ ├── pypi.go │ │ │ ├── pypi_integration_test.go │ │ │ └── pypi_test.go │ │ ├── qase/ │ │ │ ├── qase.go │ │ │ ├── qase_integration_test.go │ │ │ └── qase_test.go │ │ ├── qualaroo/ │ │ │ ├── qualaroo.go │ │ │ ├── qualaroo_integration_test.go │ │ │ └── qualaroo_test.go │ │ ├── qubole/ │ │ │ ├── qubole.go │ │ │ ├── qubole_integration_test.go │ │ │ └── qubole_test.go │ │ ├── rabbitmq/ │ │ │ ├── rabbitmq.go │ │ │ ├── rabbitmq_integration_test.go │ │ │ └── rabbitmq_test.go │ │ ├── railwayapp/ │ │ │ ├── railwayapp.go │ │ │ ├── railwayapp_integration_test.go │ │ │ └── railwayapp_test.go │ │ ├── ramp/ │ │ │ ├── ramp.go │ │ │ ├── ramp_integration_test.go │ │ │ └── ramp_test.go │ │ ├── rapidapi/ │ │ │ ├── rapidapi.go │ │ │ ├── rapidapi_integration_test.go │ │ │ └── rapidapi_test.go │ │ ├── raven/ │ │ │ ├── raven.go │ │ │ ├── raven_integration_test.go │ │ │ └── raven_test.go │ │ ├── rawg/ │ │ │ ├── rawg.go │ │ │ ├── rawg_integration_test.go │ │ │ └── rawg_test.go │ │ ├── razorpay/ │ │ │ ├── razorpay.go │ │ │ ├── razorpay_integration_test.go │ │ │ └── razorpay_test.go │ │ ├── reachmail/ │ │ │ ├── reachmail.go │ │ │ ├── reachmail_integration_test.go │ │ │ └── reachmail_test.go │ │ ├── readme/ │ │ │ ├── readme.go │ │ │ ├── readme_integration_test.go │ │ │ └── readme_test.go │ │ ├── reallysimplesystems/ │ │ │ ├── reallysimplesystems.go │ │ │ ├── reallysimplesystems_integration_test.go │ │ │ └── reallysimplesystems_test.go │ │ ├── rebrandly/ │ │ │ ├── rebrandly.go │ │ │ ├── rebrandly_integration_test.go │ │ │ └── rebrandly_test.go │ │ ├── rechargepayments/ │ │ │ ├── rechargepayments.go │ │ │ ├── rechargepayments_integration_test.go │ │ │ └── rechargepayments_test.go │ │ ├── redis/ │ │ │ ├── redis.go │ │ │ ├── redis_integration_test.go │ │ │ └── redis_test.go │ │ ├── refiner/ │ │ │ ├── refiner.go │ │ │ ├── refiner_integration_test.go │ │ │ └── refiner_test.go │ │ ├── rentman/ │ │ │ ├── rentman.go │ │ │ ├── rentman_integration_test.go │ │ │ └── rentman_test.go │ │ ├── repairshopr/ │ │ │ ├── repairshopr.go │ │ │ ├── repairshopr_integration_test.go │ │ │ └── repairshopr_test.go │ │ ├── replicate/ │ │ │ ├── replicate.go │ │ │ ├── replicate_integration_test.go │ │ │ └── replicate_test.go │ │ ├── replyio/ │ │ │ ├── replyio.go │ │ │ ├── replyio_integration_test.go │ │ │ └── replyio_test.go │ │ ├── requestfinance/ │ │ │ ├── requestfinance.go │ │ │ ├── requestfinance_integration_test.go │ │ │ └── requestfinance_test.go │ │ ├── restpackhtmltopdfapi/ │ │ │ ├── restpackhtmltopdfapi.go │ │ │ ├── restpackhtmltopdfapi_integration_test.go │ │ │ └── restpackhtmltopdfapi_test.go │ │ ├── restpackscreenshotapi/ │ │ │ ├── restpackscreenshotapi.go │ │ │ ├── restpackscreenshotapi_integration_test.go │ │ │ └── restpackscreenshotapi_test.go │ │ ├── rev/ │ │ │ ├── rev.go │ │ │ ├── rev_integration_test.go │ │ │ └── rev_test.go │ │ ├── revampcrm/ │ │ │ ├── revampcrm.go │ │ │ ├── revampcrm_integration_test.go │ │ │ └── revampcrm_test.go │ │ ├── ringcentral/ │ │ │ ├── ringcentral.go │ │ │ ├── ringcentral_integration_test.go │ │ │ └── ringcentral_test.go │ │ ├── ritekit/ │ │ │ ├── ritekit.go │ │ │ ├── ritekit_integration_test.go │ │ │ └── ritekit_test.go │ │ ├── roaring/ │ │ │ ├── roaring.go │ │ │ ├── roaring_integration_test.go │ │ │ └── roaring_test.go │ │ ├── robinhoodcrypto/ │ │ │ ├── robinhoodcrypto.go │ │ │ ├── robinhoodcrypto_integration_test.go │ │ │ └── robinhoodcrypto_test.go │ │ ├── rocketreach/ │ │ │ ├── rocketreach.go │ │ │ ├── rocketreach_integration_test.go │ │ │ └── rocketreach_test.go │ │ ├── rootly/ │ │ │ ├── rootly.go │ │ │ ├── rootly_integration_test.go │ │ │ └── rootly_test.go │ │ ├── route4me/ │ │ │ ├── route4me.go │ │ │ ├── route4me_integration_test.go │ │ │ └── route4me_test.go │ │ ├── rownd/ │ │ │ ├── rownd.go │ │ │ ├── rownd_integration_test.go │ │ │ └── rownd_test.go │ │ ├── rubygems/ │ │ │ ├── rubygems.go │ │ │ ├── rubygems_integration_test.go │ │ │ └── rubygems_test.go │ │ ├── runrunit/ │ │ │ ├── runrunit.go │ │ │ ├── runrunit_integration_test.go │ │ │ └── runrunit_test.go │ │ ├── saladcloudapikey/ │ │ │ ├── saladcloudapikey.go │ │ │ ├── saladcloudapikey_integration_test.go │ │ │ └── saladcloudapikey_test.go │ │ ├── salesblink/ │ │ │ ├── salesblink.go │ │ │ ├── salesblink_integration_test.go │ │ │ └── salesblink_test.go │ │ ├── salescookie/ │ │ │ ├── salescookie.go │ │ │ ├── salescookie_integration_test.go │ │ │ └── salescookie_test.go │ │ ├── salesflare/ │ │ │ ├── salesflare.go │ │ │ ├── salesflare_integration_test.go │ │ │ └── salesflare_test.go │ │ ├── salesforce/ │ │ │ ├── salesforce.go │ │ │ ├── salesforce_integration_test.go │ │ │ └── salesforce_test.go │ │ ├── salesforceoauth2/ │ │ │ ├── salesforceoauth2.go │ │ │ ├── salesforceoauth2_integration_test.go │ │ │ └── salesforceoauth2_test.go │ │ ├── salesforcerefreshtoken/ │ │ │ ├── salesforcerefreshtoken.go │ │ │ ├── salesforcerefreshtoken_integration_test.go │ │ │ └── salesforcerefreshtoken_test.go │ │ ├── salesmate/ │ │ │ ├── salesmate.go │ │ │ ├── salesmate_integration_test.go │ │ │ └── salesmate_test.go │ │ ├── sanity/ │ │ │ ├── sanity.go │ │ │ ├── sanity_integration_test.go │ │ │ └── sanity_test.go │ │ ├── satismeterprojectkey/ │ │ │ ├── satismeterprojectkey.go │ │ │ ├── satismeterprojectkey_integration_test.go │ │ │ └── satismeterprojectkey_test.go │ │ ├── satismeterwritekey/ │ │ │ ├── satismeterwritekey.go │ │ │ ├── satismeterwritekey_integration_test.go │ │ │ └── satismeterwritekey_test.go │ │ ├── saucelabs/ │ │ │ ├── saucelabs.go │ │ │ ├── saucelabs_integration_test.go │ │ │ └── saucelabs_test.go │ │ ├── scalewaykey/ │ │ │ ├── scalewaykey.go │ │ │ ├── scalewaykey_integration_test.go │ │ │ └── scalewaykey_test.go │ │ ├── scalr/ │ │ │ ├── scalr.go │ │ │ ├── scalr_integration_test.go │ │ │ └── scalr_test.go │ │ ├── scrapeowl/ │ │ │ ├── scrapeowl.go │ │ │ ├── scrapeowl_integration_test.go │ │ │ └── scrapeowl_test.go │ │ ├── scraperapi/ │ │ │ ├── scraperapi.go │ │ │ ├── scraperapi_integration_test.go │ │ │ └── scraperapi_test.go │ │ ├── scraperbox/ │ │ │ ├── scraperbox.go │ │ │ ├── scraperbox_integration_test.go │ │ │ └── scraperbox_test.go │ │ ├── scrapestack/ │ │ │ ├── scrapestack.go │ │ │ ├── scrapestack_integration_test.go │ │ │ └── scrapestack_test.go │ │ ├── scrapfly/ │ │ │ ├── scrapfly.go │ │ │ ├── scrapfly_integration_test.go │ │ │ └── scrapfly_test.go │ │ ├── scrapingant/ │ │ │ ├── scrapingant.go │ │ │ ├── scrapingant_integration_test.go │ │ │ └── scrapingant_test.go │ │ ├── scrapingbee/ │ │ │ ├── scrapingbee.go │ │ │ ├── scrapingbee_integration_test.go │ │ │ └── scrapingbee_test.go │ │ ├── screenshotapi/ │ │ │ ├── screenshotapi.go │ │ │ ├── screenshotapi_integration_test.go │ │ │ └── screenshotapi_test.go │ │ ├── screenshotlayer/ │ │ │ ├── screenshotlayer.go │ │ │ ├── screenshotlayer_integration_test.go │ │ │ └── screenshotlayer_test.go │ │ ├── scrutinizerci/ │ │ │ ├── scrutinizerci.go │ │ │ ├── scrutinizerci_integration_test.go │ │ │ └── scrutinizerci_test.go │ │ ├── securitytrails/ │ │ │ ├── securitytrails.go │ │ │ ├── securitytrails_integration_test.go │ │ │ └── securitytrails_test.go │ │ ├── segmentapikey/ │ │ │ ├── segmentapikey.go │ │ │ ├── segmentapikey_integration_test.go │ │ │ └── segmentapikey_test.go │ │ ├── selectpdf/ │ │ │ ├── selectpdf.go │ │ │ ├── selectpdf_integration_test.go │ │ │ └── selectpdf_test.go │ │ ├── semaphore/ │ │ │ ├── semaphore.go │ │ │ ├── semaphore_integration_test.go │ │ │ └── semaphore_test.go │ │ ├── sendbird/ │ │ │ ├── sendbird.go │ │ │ ├── sendbird_integration_test.go │ │ │ └── sendbird_test.go │ │ ├── sendbirdorganizationapi/ │ │ │ ├── sendbirdorganizationapi.go │ │ │ ├── sendbirdorganizationapi_integration_test.go │ │ │ └── sendbirdorganizationapi_test.go │ │ ├── sendgrid/ │ │ │ ├── sendgrid.go │ │ │ ├── sendgrid_integration_test.go │ │ │ └── sendgrid_test.go │ │ ├── sendinbluev2/ │ │ │ ├── sendinbluev2.go │ │ │ ├── sendinbluev2_integration_test.go │ │ │ └── sendinbluev2_test.go │ │ ├── sentryorgtoken/ │ │ │ ├── sentryorgtoken.go │ │ │ ├── sentryorgtoken_integration_test.go │ │ │ └── sentryorgtoken_test.go │ │ ├── sentrytoken/ │ │ │ ├── v1/ │ │ │ │ ├── sentrytoken.go │ │ │ │ ├── sentrytoken_integration_test.go │ │ │ │ └── sentrytoken_test.go │ │ │ └── v2/ │ │ │ ├── sentrytoken.go │ │ │ ├── sentrytoken_integration_test.go │ │ │ └── sentrytoken_test.go │ │ ├── serphouse/ │ │ │ ├── serphouse.go │ │ │ ├── serphouse_integration_test.go │ │ │ └── serphouse_test.go │ │ ├── serpstack/ │ │ │ ├── serpstack.go │ │ │ ├── serpstack_integration_test.go │ │ │ └── serpstack_test.go │ │ ├── sheety/ │ │ │ ├── sheety.go │ │ │ ├── sheety_integration_test.go │ │ │ └── sheety_test.go │ │ ├── sherpadesk/ │ │ │ ├── sherpadesk.go │ │ │ ├── sherpadesk_integration_test.go │ │ │ └── sherpadesk_test.go │ │ ├── shipday/ │ │ │ ├── shipday.go │ │ │ ├── shipday_integration_test.go │ │ │ └── shipday_test.go │ │ ├── shodankey/ │ │ │ ├── shodankey.go │ │ │ ├── shodankey_integration_test.go │ │ │ └── shodankey_test.go │ │ ├── shopify/ │ │ │ ├── shopify.go │ │ │ ├── shopify_integration_test.go │ │ │ └── shopify_test.go │ │ ├── shortcut/ │ │ │ ├── shortcut.go │ │ │ ├── shortcut_integration_test.go │ │ │ └── shortcut_test.go │ │ ├── shotstack/ │ │ │ ├── shotstack.go │ │ │ ├── shotstack_integration_test.go │ │ │ └── shotstack_test.go │ │ ├── shutterstock/ │ │ │ ├── shutterstock.go │ │ │ ├── shutterstock_integration_test.go │ │ │ └── shutterstock_test.go │ │ ├── shutterstockoauth/ │ │ │ ├── shutterstockoauth.go │ │ │ ├── shutterstockoauth_integration_test.go │ │ │ └── shutterstockoauth_test.go │ │ ├── signable/ │ │ │ ├── signable.go │ │ │ ├── signable_integration_test.go │ │ │ └── signable_test.go │ │ ├── signalwire/ │ │ │ ├── signalwire.go │ │ │ ├── signalwire_integration_test.go │ │ │ └── signalwire_test.go │ │ ├── signaturit/ │ │ │ ├── signaturit.go │ │ │ ├── signaturit_integration_test.go │ │ │ └── signaturit_test.go │ │ ├── signupgenius/ │ │ │ ├── signupgenius.go │ │ │ ├── signupgenius_integration_test.go │ │ │ └── signupgenius_test.go │ │ ├── sigopt/ │ │ │ ├── sigopt.go │ │ │ ├── sigopt_integration_test.go │ │ │ └── sigopt_test.go │ │ ├── simfin/ │ │ │ ├── simfin.go │ │ │ ├── simfin_integration_test.go │ │ │ └── simfin_test.go │ │ ├── simplesat/ │ │ │ ├── simplesat.go │ │ │ ├── simplesat_integration_test.go │ │ │ └── simplesat_test.go │ │ ├── simplynoted/ │ │ │ ├── simplynoted.go │ │ │ ├── simplynoted_integration_test.go │ │ │ └── simplynoted_test.go │ │ ├── simvoly/ │ │ │ ├── simvoly.go │ │ │ ├── simvoly_integration_test.go │ │ │ └── simvoly_test.go │ │ ├── sinchmessage/ │ │ │ ├── sinchmessage.go │ │ │ ├── sinchmessage_integration_test.go │ │ │ └── sinchmessage_test.go │ │ ├── sirv/ │ │ │ ├── sirv.go │ │ │ ├── sirv_integration_test.go │ │ │ └── sirv_test.go │ │ ├── siteleaf/ │ │ │ ├── siteleaf.go │ │ │ ├── siteleaf_integration_test.go │ │ │ └── siteleaf_test.go │ │ ├── skrappio/ │ │ │ ├── skrappio.go │ │ │ ├── skrappio_integration_test.go │ │ │ └── skrappio_test.go │ │ ├── skybiometry/ │ │ │ ├── skybiometry.go │ │ │ ├── skybiometry_integration_test.go │ │ │ └── skybiometry_test.go │ │ ├── slack/ │ │ │ ├── slack.go │ │ │ ├── slack_integration_test.go │ │ │ └── slack_test.go │ │ ├── slackwebhook/ │ │ │ ├── slackwebhook.go │ │ │ ├── slackwebhook_integration_test.go │ │ │ └── slackwebhook_test.go │ │ ├── smartsheets/ │ │ │ ├── smartsheets.go │ │ │ ├── smartsheets_integration_test.go │ │ │ └── smartsheets_test.go │ │ ├── smartystreets/ │ │ │ ├── smartystreets.go │ │ │ ├── smartystreets_integration_test.go │ │ │ └── smartystreets_test.go │ │ ├── smooch/ │ │ │ ├── smooch.go │ │ │ ├── smooch_integration_test.go │ │ │ └── smooch_test.go │ │ ├── snipcart/ │ │ │ ├── snipcart.go │ │ │ ├── snipcart_integration_test.go │ │ │ └── snipcart_test.go │ │ ├── snowflake/ │ │ │ ├── snowflake.go │ │ │ ├── snowflake_integration_test.go │ │ │ └── snowflake_test.go │ │ ├── snykkey/ │ │ │ ├── snykkey.go │ │ │ ├── snykkey_integration_test.go │ │ │ └── snykkey_test.go │ │ ├── sonarcloud/ │ │ │ ├── sonarcloud.go │ │ │ ├── sonarcloud_integration_test.go │ │ │ └── sonarcloud_test.go │ │ ├── sourcegraph/ │ │ │ ├── sourcegraph.go │ │ │ ├── sourcegraph_integration_test.go │ │ │ └── sourcegraph_test.go │ │ ├── sourcegraphcody/ │ │ │ ├── sourcegraphcody.go │ │ │ ├── sourcegraphcody_integration_test.go │ │ │ └── sourcegraphcody_test.go │ │ ├── sparkpost/ │ │ │ ├── sparkpost.go │ │ │ ├── sparkpost_integration_test.go │ │ │ └── sparkpost_test.go │ │ ├── speechtextai/ │ │ │ ├── speechtextai.go │ │ │ ├── speechtextai_integration_test.go │ │ │ └── speechtextai_test.go │ │ ├── splunkobservabilitytoken/ │ │ │ ├── splunkobservabilitytoken.go │ │ │ ├── splunkobservabilitytoken_integration_test.go │ │ │ └── splunkobservabilitytoken_test.go │ │ ├── spoonacular/ │ │ │ ├── spoonacular.go │ │ │ ├── spoonacular_integration_test.go │ │ │ └── spoonacular_test.go │ │ ├── sportsmonk/ │ │ │ ├── sportsmonk.go │ │ │ ├── sportsmonk_integration_test.go │ │ │ └── sportsmonk_test.go │ │ ├── spotifykey/ │ │ │ ├── spotifykey.go │ │ │ ├── spotifykey_integration_test.go │ │ │ └── spotifykey_test.go │ │ ├── sqlserver/ │ │ │ ├── sqlserver.go │ │ │ ├── sqlserver_integration_test.go │ │ │ └── sqlserver_test.go │ │ ├── square/ │ │ │ ├── square.go │ │ │ ├── square_integration_test.go │ │ │ └── square_test.go │ │ ├── squareapp/ │ │ │ ├── squareapp.go │ │ │ ├── squareapp_integration_test.go │ │ │ └── squareapp_test.go │ │ ├── squarespace/ │ │ │ ├── squarespace.go │ │ │ ├── squarespace_integration_test.go │ │ │ └── squarespace_test.go │ │ ├── squareup/ │ │ │ ├── squareup.go │ │ │ ├── squareup_integration_test.go │ │ │ └── squareup_test.go │ │ ├── sslmate/ │ │ │ ├── sslmate.go │ │ │ ├── sslmate_integration_test.go │ │ │ └── sslmate_test.go │ │ ├── statuscake/ │ │ │ ├── statuscake.go │ │ │ ├── statuscake_integration_test.go │ │ │ └── statuscake_test.go │ │ ├── statuspage/ │ │ │ ├── statuspage.go │ │ │ ├── statuspage_integration_test.go │ │ │ └── statuspage_test.go │ │ ├── statuspal/ │ │ │ ├── statuspal.go │ │ │ ├── statuspal_integration_test.go │ │ │ └── statuspal_test.go │ │ ├── stitchdata/ │ │ │ ├── stitchdata.go │ │ │ ├── stitchdata_integration_test.go │ │ │ └── stitchdata_test.go │ │ ├── stockdata/ │ │ │ ├── stockdata.go │ │ │ ├── stockdata_integration_test.go │ │ │ └── stockdata_test.go │ │ ├── storecove/ │ │ │ ├── storecove.go │ │ │ ├── storecove_integration_test.go │ │ │ └── storecove_test.go │ │ ├── stormboard/ │ │ │ ├── stormboard.go │ │ │ ├── stormboard_integration_test.go │ │ │ └── stormboard_test.go │ │ ├── stormglass/ │ │ │ ├── stormglass.go │ │ │ ├── stormglass_integration_test.go │ │ │ └── stormglass_test.go │ │ ├── storyblok/ │ │ │ ├── storyblok.go │ │ │ ├── storyblok_integration_test.go │ │ │ └── storyblok_test.go │ │ ├── storyblokpersonalaccesstoken/ │ │ │ ├── storyblok.go │ │ │ ├── storyblok_integration_test.go │ │ │ └── storyblok_test.go │ │ ├── storychief/ │ │ │ ├── storychief.go │ │ │ ├── storychief_integration_test.go │ │ │ └── storychief_test.go │ │ ├── strava/ │ │ │ ├── strava.go │ │ │ ├── strava_integration_test.go │ │ │ └── strava_test.go │ │ ├── streak/ │ │ │ ├── streak.go │ │ │ ├── streak_integration_test.go │ │ │ └── streak_test.go │ │ ├── stripe/ │ │ │ ├── stripe.go │ │ │ ├── stripe_integration_test.go │ │ │ └── stripe_test.go │ │ ├── stripepaymentintent/ │ │ │ ├── stripepaymentintent.go │ │ │ ├── stripepaymentintent_integration_test.go │ │ │ └── stripepaymentintent_test.go │ │ ├── stripo/ │ │ │ ├── stripo.go │ │ │ ├── stripo_integration_test.go │ │ │ └── stripo_test.go │ │ ├── stytch/ │ │ │ ├── stytch.go │ │ │ ├── stytch_integration_test.go │ │ │ └── stytch_test.go │ │ ├── sugester/ │ │ │ ├── sugester.go │ │ │ ├── sugester_integration_test.go │ │ │ └── sugester_test.go │ │ ├── sumologickey/ │ │ │ ├── sumologickey.go │ │ │ ├── sumologickey_integration_test.go │ │ │ └── sumologickey_test.go │ │ ├── supabasetoken/ │ │ │ ├── supabasetoken.go │ │ │ ├── supabasetoken_integration_test.go │ │ │ └── supabasetoken_test.go │ │ ├── supernotesapi/ │ │ │ ├── supernotesapi.go │ │ │ ├── supernotesapi_integration_test.go │ │ │ └── supernotesapi_test.go │ │ ├── surveyanyplace/ │ │ │ ├── surveyanyplace.go │ │ │ ├── surveyanyplace_integration_test.go │ │ │ └── surveyanyplace_test.go │ │ ├── surveybot/ │ │ │ ├── surveybot.go │ │ │ ├── surveybot_integration_test.go │ │ │ └── surveybot_test.go │ │ ├── surveysparrow/ │ │ │ ├── surveysparrow.go │ │ │ ├── surveysparrow_integration_test.go │ │ │ └── surveysparrow_test.go │ │ ├── survicate/ │ │ │ ├── survicate.go │ │ │ ├── survicate_integration_test.go │ │ │ └── survicate_test.go │ │ ├── swell/ │ │ │ ├── swell.go │ │ │ ├── swell_integration_test.go │ │ │ └── swell_test.go │ │ ├── swiftype/ │ │ │ ├── swiftype.go │ │ │ ├── swiftype_integration_test.go │ │ │ └── swiftype_test.go │ │ ├── tableau/ │ │ │ ├── tableau.go │ │ │ ├── tableau_integration_test.go │ │ │ └── tableau_test.go │ │ ├── tailscale/ │ │ │ ├── tailscale.go │ │ │ ├── tailscale_integration_test.go │ │ │ └── tailscale_test.go │ │ ├── tallyfy/ │ │ │ ├── tallyfy.go │ │ │ ├── tallyfy_integration_test.go │ │ │ └── tallyfy_test.go │ │ ├── tatumio/ │ │ │ ├── tatumio.go │ │ │ ├── tatumio_integration_test.go │ │ │ └── tatumio_test.go │ │ ├── taxjar/ │ │ │ ├── taxjar.go │ │ │ ├── taxjar_integration_test.go │ │ │ └── taxjar_test.go │ │ ├── teamgate/ │ │ │ ├── teamgate.go │ │ │ ├── teamgate_integration_test.go │ │ │ └── teamgate_test.go │ │ ├── teamworkcrm/ │ │ │ ├── teamworkcrm.go │ │ │ ├── teamworkcrm_integration_test.go │ │ │ └── teamworkcrm_test.go │ │ ├── teamworkdesk/ │ │ │ ├── teamworkdesk.go │ │ │ ├── teamworkdesk_integration_test.go │ │ │ └── teamworkdesk_test.go │ │ ├── teamworkspaces/ │ │ │ ├── teamworkspaces.go │ │ │ ├── teamworkspaces_integration_test.go │ │ │ └── teamworkspaces_test.go │ │ ├── technicalanalysisapi/ │ │ │ ├── technicalanalysisapi.go │ │ │ ├── technicalanalysisapi_integration_test.go │ │ │ └── technicalanalysisapi_test.go │ │ ├── tefter/ │ │ │ ├── tefter.go │ │ │ ├── tefter_integration_test.go │ │ │ └── tefter_test.go │ │ ├── telegrambottoken/ │ │ │ ├── telegrambottoken.go │ │ │ ├── telegrambottoken_integration_test.go │ │ │ └── telegrambottoken_test.go │ │ ├── teletype/ │ │ │ ├── teletype.go │ │ │ ├── teletype_integration_test.go │ │ │ └── teletype_test.go │ │ ├── telnyx/ │ │ │ ├── telnyx.go │ │ │ ├── telnyx_integration_test.go │ │ │ └── telnyx_test.go │ │ ├── terraformcloudpersonaltoken/ │ │ │ ├── terraformcloudpersonaltoken.go │ │ │ ├── terraformcloudpersonaltoken_integration_test.go │ │ │ └── terraformcloudpersonaltoken_test.go │ │ ├── testingbot/ │ │ │ ├── testingbot.go │ │ │ ├── testingbot_integration_test.go │ │ │ └── testingbot_test.go │ │ ├── textmagic/ │ │ │ ├── textmagic.go │ │ │ ├── textmagic_integration_test.go │ │ │ └── textmagic_test.go │ │ ├── theoddsapi/ │ │ │ ├── theoddsapi.go │ │ │ ├── theoddsapi_integration_test.go │ │ │ └── theoddsapi_test.go │ │ ├── thinkific/ │ │ │ ├── thinkific.go │ │ │ ├── thinkific_integration_test.go │ │ │ └── thinkific_test.go │ │ ├── thousandeyes/ │ │ │ ├── thousandeyes.go │ │ │ ├── thousandeyes_integration_test.go │ │ │ └── thousandeyes_test.go │ │ ├── ticketmaster/ │ │ │ ├── ticketmaster.go │ │ │ ├── ticketmaster_integration_test.go │ │ │ └── ticketmaster_test.go │ │ ├── tickettailor/ │ │ │ ├── tickettailor.go │ │ │ ├── tickettailor_integration_test.go │ │ │ └── tickettailor_test.go │ │ ├── tiingo/ │ │ │ ├── tiingo.go │ │ │ ├── tiingo_integration_test.go │ │ │ └── tiingo_test.go │ │ ├── timecamp/ │ │ │ ├── timecamp.go │ │ │ ├── timecamp_integration_test.go │ │ │ └── timecamp_test.go │ │ ├── timezoneapi/ │ │ │ ├── timezoneapi.go │ │ │ ├── timezoneapi_integration_test.go │ │ │ └── timezoneapi_test.go │ │ ├── tineswebhook/ │ │ │ ├── tineswebhook.go │ │ │ ├── tineswebhook_integration_test.go │ │ │ └── tineswebhook_test.go │ │ ├── tly/ │ │ │ ├── tly.go │ │ │ ├── tly_integration_test.go │ │ │ └── tly_test.go │ │ ├── tmetric/ │ │ │ ├── tmetric.go │ │ │ ├── tmetric_integration_test.go │ │ │ └── tmetric_test.go │ │ ├── todoist/ │ │ │ ├── todoist.go │ │ │ ├── todoist_integration_test.go │ │ │ └── todoist_test.go │ │ ├── toggltrack/ │ │ │ ├── toggltrack.go │ │ │ ├── toggltrack_integration_test.go │ │ │ └── toggltrack_test.go │ │ ├── tokeet/ │ │ │ ├── tokeet.go │ │ │ ├── tokeet_integration_test.go │ │ │ └── tokeet_test.go │ │ ├── tomorrowio/ │ │ │ ├── tomorrowio.go │ │ │ ├── tomorrowio_integration_test.go │ │ │ └── tomorrowio_test.go │ │ ├── tomtom/ │ │ │ ├── tomtom.go │ │ │ ├── tomtom_integration_test.go │ │ │ └── tomtom_test.go │ │ ├── tradier/ │ │ │ ├── tradier.go │ │ │ ├── tradier_integration_test.go │ │ │ └── tradier_test.go │ │ ├── transferwise/ │ │ │ ├── transferwise.go │ │ │ ├── transferwise_integration_test.go │ │ │ └── transferwise_test.go │ │ ├── travelpayouts/ │ │ │ ├── travelpayouts.go │ │ │ ├── travelpayouts_integration_test.go │ │ │ └── travelpayouts_test.go │ │ ├── travisci/ │ │ │ ├── travisci.go │ │ │ ├── travisci_integration_test.go │ │ │ └── travisci_test.go │ │ ├── trelloapikey/ │ │ │ ├── trelloapikey.go │ │ │ ├── trelloapikey_integration_test.go │ │ │ └── trelloapikey_test.go │ │ ├── tru/ │ │ │ ├── tru.go │ │ │ ├── tru_integration_test.go │ │ │ └── tru_test.go │ │ ├── trufflehogenterprise/ │ │ │ ├── trufflehogenterprise.go │ │ │ ├── trufflehogenterprise_integration_test.go │ │ │ └── trufflehogenterprise_test.go │ │ ├── twelvedata/ │ │ │ ├── twelvedata.go │ │ │ ├── twelvedata_integration_test.go │ │ │ └── twelvedata_test.go │ │ ├── twilio/ │ │ │ ├── twilio.go │ │ │ ├── twilio_integration_test.go │ │ │ └── twilio_test.go │ │ ├── twilioapikey/ │ │ │ ├── twilioapikey.go │ │ │ ├── twilioapikey_integration_test.go │ │ │ └── twilioapikey_test.go │ │ ├── twist/ │ │ │ ├── twist.go │ │ │ ├── twist_integration_test.go │ │ │ └── twist_test.go │ │ ├── twitch/ │ │ │ ├── twitch.go │ │ │ ├── twitch_integration_test.go │ │ │ └── twitch_test.go │ │ ├── twitchaccesstoken/ │ │ │ ├── twitchaccesstoken.go │ │ │ ├── twitchaccesstoken_integration_test.go │ │ │ └── twitchaccesstoken_test.go │ │ ├── twitter/ │ │ │ ├── v1/ │ │ │ │ ├── twitter_v1.go │ │ │ │ ├── twitter_v1_integration_test.go │ │ │ │ └── twitter_v1_test.go │ │ │ └── v2/ │ │ │ ├── twitter_v2.go │ │ │ ├── twitter_v2_integration_test.go │ │ │ └── twitter_v2_test.go │ │ ├── twitterconsumerkey/ │ │ │ ├── twitterconsumerkey.go │ │ │ ├── twitterconsumerkey_integration_test.go │ │ │ └── twitterconsumerkey_test.go │ │ ├── tyntec/ │ │ │ ├── tyntec.go │ │ │ ├── tyntec_integration_test.go │ │ │ └── tyntec_test.go │ │ ├── typeform/ │ │ │ ├── v1/ │ │ │ │ ├── typeform.go │ │ │ │ ├── typeform_integration_test.go │ │ │ │ └── typeform_test.go │ │ │ └── v2/ │ │ │ ├── typeform.go │ │ │ ├── typeform_integration_test.go │ │ │ └── typeform_test.go │ │ ├── typetalk/ │ │ │ ├── typetalk.go │ │ │ ├── typetalk_integration_test.go │ │ │ └── typetalk_test.go │ │ ├── ubidots/ │ │ │ ├── ubidots.go │ │ │ ├── ubidots_integration_test.go │ │ │ └── ubidots_test.go │ │ ├── uclassify/ │ │ │ ├── uclassify.go │ │ │ ├── uclassify_integration_test.go │ │ │ └── uclassify_test.go │ │ ├── unifyid/ │ │ │ ├── unifyid.go │ │ │ ├── unifyid_integration_test.go │ │ │ └── unifyid_test.go │ │ ├── unplugg/ │ │ │ ├── unplugg.go │ │ │ ├── unplugg_integration_test.go │ │ │ └── unplugg_test.go │ │ ├── unsplash/ │ │ │ ├── unsplash.go │ │ │ ├── unsplash_integration_test.go │ │ │ └── unsplash_test.go │ │ ├── upcdatabase/ │ │ │ ├── upcdatabase.go │ │ │ ├── upcdatabase_integration_test.go │ │ │ └── upcdatabase_test.go │ │ ├── uplead/ │ │ │ ├── uplead.go │ │ │ ├── uplead_integration_test.go │ │ │ └── uplead_test.go │ │ ├── uploadcare/ │ │ │ ├── uploadcare.go │ │ │ ├── uploadcare_integration_test.go │ │ │ └── uploadcare_test.go │ │ ├── uptimerobot/ │ │ │ ├── uptimerobot.go │ │ │ ├── uptimerobot_integration_test.go │ │ │ └── uptimerobot_test.go │ │ ├── upwave/ │ │ │ ├── upwave.go │ │ │ ├── upwave_integration_test.go │ │ │ └── upwave_test.go │ │ ├── uri/ │ │ │ ├── uri.go │ │ │ ├── uri_integration_test.go │ │ │ └── uri_test.go │ │ ├── urlscan/ │ │ │ ├── urlscan.go │ │ │ ├── urlscan_integration_test.go │ │ │ └── urlscan_test.go │ │ ├── user/ │ │ │ ├── user.go │ │ │ ├── user_integration_test.go │ │ │ └── user_test.go │ │ ├── userflow/ │ │ │ ├── userflow.go │ │ │ ├── userflow_integration_test.go │ │ │ └── userflow_test.go │ │ ├── userstack/ │ │ │ ├── userstack.go │ │ │ ├── userstack_integration_test.go │ │ │ └── userstack_test.go │ │ ├── vagrantcloudpersonaltoken/ │ │ │ ├── vagrantcloudpersonaltoken.go │ │ │ ├── vagrantcloudpersonaltoken_integration_test.go │ │ │ └── vagrantcloudpersonaltoken_test.go │ │ ├── vatlayer/ │ │ │ ├── vatlayer.go │ │ │ ├── vatlayer_integration_test.go │ │ │ └── vatlayer_test.go │ │ ├── vbout/ │ │ │ ├── vbout.go │ │ │ ├── vbout_integration_test.go │ │ │ └── vbout_test.go │ │ ├── vercel/ │ │ │ ├── vercel.go │ │ │ ├── vercel_integration_test.go │ │ │ └── vercel_test.go │ │ ├── verifier/ │ │ │ ├── verifier.go │ │ │ ├── verifier_integration_test.go │ │ │ └── verifier_test.go │ │ ├── verimail/ │ │ │ ├── verimail.go │ │ │ ├── verimail_integration_test.go │ │ │ └── verimail_test.go │ │ ├── veriphone/ │ │ │ ├── veriphone.go │ │ │ ├── veriphone_integration_test.go │ │ │ └── veriphone_test.go │ │ ├── versioneye/ │ │ │ ├── versioneye.go │ │ │ ├── versioneye_integration_test.go │ │ │ └── versioneye_test.go │ │ ├── viewneo/ │ │ │ ├── viewneo.go │ │ │ ├── viewneo_integration_test.go │ │ │ └── viewneo_test.go │ │ ├── virustotal/ │ │ │ ├── virustotal.go │ │ │ ├── virustotal_integration_test.go │ │ │ └── virustotal_test.go │ │ ├── visualcrossing/ │ │ │ ├── visualcrossing.go │ │ │ ├── visualcrossing_integration_test.go │ │ │ └── visualcrossing_test.go │ │ ├── voiceflow/ │ │ │ ├── voiceflow.go │ │ │ ├── voiceflow_integration_test.go │ │ │ └── voiceflow_test.go │ │ ├── voicegain/ │ │ │ ├── voicegain.go │ │ │ ├── voicegain_integration_test.go │ │ │ └── voicegain_test.go │ │ ├── voodoosms/ │ │ │ ├── voodoosms.go │ │ │ ├── voodoosms_integration_test.go │ │ │ └── voodoosms_test.go │ │ ├── vouchery/ │ │ │ ├── vouchery.go │ │ │ ├── vouchery_integration_test.go │ │ │ └── vouchery_test.go │ │ ├── vpnapi/ │ │ │ ├── vpnapi.go │ │ │ ├── vpnapi_integration_test.go │ │ │ └── vpnapi_test.go │ │ ├── vultrapikey/ │ │ │ ├── vultrapikey.go │ │ │ ├── vultrapikey_integration_test.go │ │ │ └── vultrapikey_test.go │ │ ├── vyte/ │ │ │ ├── vyte.go │ │ │ ├── vyte_integration_test.go │ │ │ └── vyte_test.go │ │ ├── walkscore/ │ │ │ ├── walkscore.go │ │ │ ├── walkscore_integration_test.go │ │ │ └── walkscore_test.go │ │ ├── weatherbit/ │ │ │ ├── weatherbit.go │ │ │ ├── weatherbit_integration_test.go │ │ │ └── weatherbit_test.go │ │ ├── weatherstack/ │ │ │ ├── weatherstack.go │ │ │ ├── weatherstack_integration_test.go │ │ │ └── weatherstack_test.go │ │ ├── web3storage/ │ │ │ ├── web3storage.go │ │ │ ├── web3storage_integration_test.go │ │ │ └── web3storage_test.go │ │ ├── webex/ │ │ │ ├── webex.go │ │ │ ├── webex_integration_test.go │ │ │ └── webex_test.go │ │ ├── webexbot/ │ │ │ ├── webexbot.go │ │ │ ├── webexbot_integration_test.go │ │ │ └── webexbot_test.go │ │ ├── webflow/ │ │ │ ├── webflow.go │ │ │ ├── webflow_integration_test.go │ │ │ └── webflow_test.go │ │ ├── webscraper/ │ │ │ ├── webscraper.go │ │ │ ├── webscraper_integration_test.go │ │ │ └── webscraper_test.go │ │ ├── webscraping/ │ │ │ ├── webscraping.go │ │ │ ├── webscraping_integration_test.go │ │ │ └── webscraping_test.go │ │ ├── websitepulse/ │ │ │ ├── websitepulse.go │ │ │ ├── websitepulse_integration_test.go │ │ │ └── websitepulse_test.go │ │ ├── weightsandbiases/ │ │ │ ├── weightsandbiases.go │ │ │ ├── weightsandbiases_integration_test.go │ │ │ └── weightsandbiases_test.go │ │ ├── wepay/ │ │ │ ├── wepay.go │ │ │ ├── wepay_integration_test.go │ │ │ └── wepay_test.go │ │ ├── whoxy/ │ │ │ ├── whoxy.go │ │ │ ├── whoxy_integration_test.go │ │ │ └── whoxy_test.go │ │ ├── wistia/ │ │ │ ├── wistia.go │ │ │ ├── wistia_integration_test.go │ │ │ └── wistia_test.go │ │ ├── wit/ │ │ │ ├── wit.go │ │ │ ├── wit_integration_test.go │ │ │ └── wit_test.go │ │ ├── wiz/ │ │ │ ├── wiz.go │ │ │ ├── wiz_integration_test.go │ │ │ └── wiz_test.go │ │ ├── worksnaps/ │ │ │ ├── worksnaps.go │ │ │ ├── worksnaps_integration_test.go │ │ │ └── worksnaps_test.go │ │ ├── workstack/ │ │ │ ├── workstack.go │ │ │ ├── workstack_integration_test.go │ │ │ └── workstack_test.go │ │ ├── worldcoinindex/ │ │ │ ├── worldcoinindex.go │ │ │ ├── worldcoinindex_integration_test.go │ │ │ └── worldcoinindex_test.go │ │ ├── worldweather/ │ │ │ ├── worldweather.go │ │ │ ├── worldweather_integration_test.go │ │ │ └── worldweather_test.go │ │ ├── wrike/ │ │ │ ├── wrike.go │ │ │ ├── wrike_integration_test.go │ │ │ └── wrike_test.go │ │ ├── xai/ │ │ │ ├── xai.go │ │ │ ├── xai_integration_test.go │ │ │ └── xai_test.go │ │ ├── yandex/ │ │ │ ├── yandex.go │ │ │ ├── yandex_integration_test.go │ │ │ └── yandex_test.go │ │ ├── yelp/ │ │ │ ├── yelp.go │ │ │ ├── yelp_integration_test.go │ │ │ └── yelp_test.go │ │ ├── youneedabudget/ │ │ │ ├── youneedabudget.go │ │ │ ├── youneedabudget_integration_test.go │ │ │ └── youneedabudget_test.go │ │ ├── yousign/ │ │ │ ├── yousign.go │ │ │ ├── yousign_integration_test.go │ │ │ └── yousign_test.go │ │ ├── youtubeapikey/ │ │ │ ├── youtubeapikey.go │ │ │ ├── youtubeapikey_integration_test.go │ │ │ └── youtubeapikey_test.go │ │ ├── zapierwebhook/ │ │ │ ├── zapierwebhook.go │ │ │ ├── zapierwebhook_integration_test.go │ │ │ └── zapierwebhook_test.go │ │ ├── zendeskapi/ │ │ │ ├── zendeskapi.go │ │ │ ├── zendeskapi_integration_test.go │ │ │ └── zendeskapi_test.go │ │ ├── zenkitapi/ │ │ │ ├── zenkitapi.go │ │ │ ├── zenkitapi_integration_test.go │ │ │ └── zenkitapi_test.go │ │ ├── zenrows/ │ │ │ ├── zenrows.go │ │ │ ├── zenrows_integration_test.go │ │ │ └── zenrows_test.go │ │ ├── zenscrape/ │ │ │ ├── zenscrape.go │ │ │ ├── zenscrape_integration_test.go │ │ │ └── zenscrape_test.go │ │ ├── zenserp/ │ │ │ ├── zenserp.go │ │ │ ├── zenserp_integration_test.go │ │ │ └── zenserp_test.go │ │ ├── zeplin/ │ │ │ ├── zeplin.go │ │ │ ├── zeplin_integration_test.go │ │ │ └── zeplin_test.go │ │ ├── zerobounce/ │ │ │ ├── zerobounce.go │ │ │ ├── zerobounce_integration_test.go │ │ │ └── zerobounce_test.go │ │ ├── zerotier/ │ │ │ ├── zerotier.go │ │ │ ├── zerotier_integration_test.go │ │ │ └── zerotier_test.go │ │ ├── zipapi/ │ │ │ ├── zipapi.go │ │ │ ├── zipapi_integration_test.go │ │ │ └── zipapi_test.go │ │ ├── zipbooks/ │ │ │ ├── zipbooks.go │ │ │ ├── zipbooks_integration_test.go │ │ │ └── zipbooks_test.go │ │ ├── zipcodeapi/ │ │ │ ├── zipcodeapi.go │ │ │ ├── zipcodeapi_integration_test.go │ │ │ └── zipcodeapi_test.go │ │ ├── zipcodebase/ │ │ │ ├── zipcodebase.go │ │ │ ├── zipcodebase_integration_test.go │ │ │ └── zipcodebase_test.go │ │ ├── zohocrm/ │ │ │ ├── zohocrm.go │ │ │ ├── zohocrm_integration_test.go │ │ │ └── zohocrm_test.go │ │ ├── zonkafeedback/ │ │ │ ├── zonkafeedback.go │ │ │ ├── zonkafeedback_integration_test.go │ │ │ └── zonkafeedback_test.go │ │ └── zulipchat/ │ │ ├── zulipchat.go │ │ ├── zulipchat_integration_test.go │ │ └── zulipchat_test.go │ ├── engine/ │ │ ├── ahocorasick/ │ │ │ ├── ahocorasickcore.go │ │ │ └── ahocorasickcore_test.go │ │ ├── circleci.go │ │ ├── defaults/ │ │ │ ├── defaults.go │ │ │ └── defaults_test.go │ │ ├── docker.go │ │ ├── elasticsearch.go │ │ ├── engine.go │ │ ├── engine_test.go │ │ ├── filesystem.go │ │ ├── filesystem_integration_test.go │ │ ├── gcs.go │ │ ├── gcs_test.go │ │ ├── git.go │ │ ├── git_test.go │ │ ├── github.go │ │ ├── github_experimental.go │ │ ├── gitlab.go │ │ ├── gitlab_integration_test.go │ │ ├── huggingface.go │ │ ├── jenkins.go │ │ ├── json_enumerator.go │ │ ├── metrics.go │ │ ├── postman.go │ │ ├── postman_test.go │ │ ├── s3.go │ │ ├── scan.go │ │ ├── stdin.go │ │ ├── syslog.go │ │ ├── testdata/ │ │ │ ├── secrets.txt │ │ │ ├── verificationoverlap_detectors.yaml │ │ │ ├── verificationoverlap_secrets.txt │ │ │ └── verificationoverlap_secrets_fp.txt │ │ └── travisci.go │ ├── feature/ │ │ └── feature.go │ ├── gitparse/ │ │ ├── gitparse.go │ │ └── gitparse_test.go │ ├── giturl/ │ │ ├── giturl.go │ │ └── giturl_test.go │ ├── handlers/ │ │ ├── apk.go │ │ ├── apk_test.go │ │ ├── ar.go │ │ ├── ar_test.go │ │ ├── archive.go │ │ ├── archive_test.go │ │ ├── default.go │ │ ├── default_test.go │ │ ├── handlers.go │ │ ├── handlers_test.go │ │ ├── metrics.go │ │ ├── rpm.go │ │ ├── rpm_test.go │ │ └── testdata/ │ │ ├── nonarchive.txt │ │ ├── test.deb │ │ ├── test.doc │ │ ├── test.msg │ │ ├── test.rpm │ │ └── test.tgz │ ├── hasher/ │ │ ├── blake2b.go │ │ ├── hasher.go │ │ └── hasher_test.go │ ├── iobuf/ │ │ ├── bufferedreaderseeker.go │ │ └── bufferedreaderseeker_test.go │ ├── log/ │ │ ├── dynamic_redactor.go │ │ ├── level.go │ │ ├── log.go │ │ ├── log_cores_test.go │ │ ├── log_test.go │ │ ├── redaction_core.go │ │ └── suppress_caller_core.go │ ├── output/ │ │ ├── github_actions.go │ │ ├── json.go │ │ ├── legacy_json.go │ │ └── plain.go │ ├── pb/ │ │ ├── configpb/ │ │ │ ├── config.pb.go │ │ │ └── config.pb.validate.go │ │ ├── credentialspb/ │ │ │ ├── credentials.pb.go │ │ │ └── credentials.pb.validate.go │ │ ├── custom_detectorspb/ │ │ │ ├── custom_detectors.pb.go │ │ │ └── custom_detectors.pb.validate.go │ │ ├── detectorspb/ │ │ │ ├── detectors.pb.go │ │ │ └── detectors.pb.validate.go │ │ ├── source_metadatapb/ │ │ │ ├── source_metadata.pb.go │ │ │ └── source_metadata.pb.validate.go │ │ └── sourcespb/ │ │ ├── sources.pb.go │ │ └── sources.pb.validate.go │ ├── process/ │ │ └── zombies.go │ ├── protoyaml/ │ │ └── protoyaml.go │ ├── roundtripper/ │ │ └── roundtripper.go │ ├── sanitizer/ │ │ ├── utf8.go │ │ └── utf8_test.go │ ├── sources/ │ │ ├── chunker.go │ │ ├── chunker_test.go │ │ ├── circleci/ │ │ │ ├── circleci.go │ │ │ └── circleci_test.go │ │ ├── docker/ │ │ │ ├── README.md │ │ │ ├── docker.go │ │ │ ├── docker_test.go │ │ │ ├── metrics.go │ │ │ ├── registries.go │ │ │ └── registries_test.go │ │ ├── elasticsearch/ │ │ │ ├── api.go │ │ │ ├── elasticsearch.go │ │ │ ├── elasticsearch_integration_test.go │ │ │ ├── unit_of_work.go │ │ │ └── unit_of_work_test.go │ │ ├── errors.go │ │ ├── errors_test.go │ │ ├── filesystem/ │ │ │ ├── filesystem.go │ │ │ ├── filesystem_symlink_test.go │ │ │ └── filesystem_test.go │ │ ├── gcs/ │ │ │ ├── gcs.go │ │ │ ├── gcs_integration_test.go │ │ │ ├── gcs_manager.go │ │ │ ├── gcs_manager_test.go │ │ │ └── gcs_test.go │ │ ├── git/ │ │ │ ├── cmd_check.go │ │ │ ├── git.go │ │ │ ├── git_test.go │ │ │ ├── metrics.go │ │ │ ├── scan_options.go │ │ │ ├── unit.go │ │ │ └── unit_test.go │ │ ├── github/ │ │ │ ├── connector.go │ │ │ ├── connector_app.go │ │ │ ├── connector_basicauth.go │ │ │ ├── connector_token.go │ │ │ ├── connector_unauthenticated.go │ │ │ ├── github.go │ │ │ ├── github_integration_test.go │ │ │ ├── github_test.go │ │ │ ├── graphql.go │ │ │ ├── metrics.go │ │ │ ├── queries.go │ │ │ └── repo.go │ │ ├── github_experimental/ │ │ │ ├── github_experimental.go │ │ │ ├── object_discovery.go │ │ │ └── repo.go │ │ ├── gitlab/ │ │ │ ├── gitlab.go │ │ │ ├── gitlab_integration_test.go │ │ │ ├── gitlab_test.go │ │ │ ├── metrics.go │ │ │ └── project_cache.go │ │ ├── huggingface/ │ │ │ ├── client.go │ │ │ ├── huggingface.go │ │ │ ├── huggingface_client_test.go │ │ │ ├── huggingface_test.go │ │ │ └── repo.go │ │ ├── jenkins/ │ │ │ ├── jenkins.go │ │ │ ├── jenkins_integration_test.go │ │ │ └── jenkins_test.go │ │ ├── job_progress.go │ │ ├── job_progress_hook.go │ │ ├── job_progress_test.go │ │ ├── json_enumerator/ │ │ │ ├── json_enumerator.go │ │ │ └── json_enumerator_test.go │ │ ├── legacy_reporters.go │ │ ├── metrics.go │ │ ├── mock_job_progress_test.go │ │ ├── postman/ │ │ │ ├── metrics.go │ │ │ ├── postman.go │ │ │ ├── postman_client.go │ │ │ ├── postman_test.go │ │ │ ├── substitution.go │ │ │ └── substitution_test.go │ │ ├── resume.go │ │ ├── resume_test.go │ │ ├── s3/ │ │ │ ├── checkpointer.go │ │ │ ├── checkpointer_test.go │ │ │ ├── metrics.go │ │ │ ├── s3.go │ │ │ ├── s3_integration_test.go │ │ │ ├── s3_test.go │ │ │ ├── unit.go │ │ │ └── unit_test.go │ │ ├── source_manager.go │ │ ├── source_manager_test.go │ │ ├── source_unit.go │ │ ├── sources.go │ │ ├── sources_test.go │ │ ├── stdin/ │ │ │ └── stdin.go │ │ ├── syslog/ │ │ │ ├── syslog.go │ │ │ └── syslog_test.go │ │ ├── test_helpers.go │ │ └── travisci/ │ │ ├── travisci.go │ │ └── travisci_test.go │ ├── sourcestest/ │ │ └── sourcestest.go │ ├── tui/ │ │ ├── common/ │ │ │ ├── common.go │ │ │ ├── component.go │ │ │ ├── error.go │ │ │ ├── style.go │ │ │ └── utils.go │ │ ├── components/ │ │ │ ├── confirm/ │ │ │ │ └── confirm.go │ │ │ ├── footer/ │ │ │ │ └── footer.go │ │ │ ├── formfield/ │ │ │ │ └── formfield.go │ │ │ ├── header/ │ │ │ │ └── header.go │ │ │ ├── selector/ │ │ │ │ └── selector.go │ │ │ ├── statusbar/ │ │ │ │ └── statusbar.go │ │ │ ├── tabs/ │ │ │ │ └── tabs.go │ │ │ ├── textinput/ │ │ │ │ └── textinput.go │ │ │ ├── textinputs/ │ │ │ │ └── textinputs.go │ │ │ └── viewport/ │ │ │ └── viewport.go │ │ ├── keymap/ │ │ │ └── keymap.go │ │ ├── pages/ │ │ │ ├── analyze_form/ │ │ │ │ └── analyze_form.go │ │ │ ├── analyze_keys/ │ │ │ │ └── analyze_keys.go │ │ │ ├── contact_enterprise/ │ │ │ │ └── contact_enterprise.go │ │ │ ├── source_configure/ │ │ │ │ ├── item.go │ │ │ │ ├── run_component.go │ │ │ │ ├── source_component.go │ │ │ │ ├── source_configure.go │ │ │ │ ├── trufflehog_component.go │ │ │ │ └── trufflehog_configure.go │ │ │ ├── source_select/ │ │ │ │ ├── item.go │ │ │ │ └── source_select.go │ │ │ ├── view_oss/ │ │ │ │ └── view_oss.go │ │ │ └── wizard_intro/ │ │ │ ├── item.go │ │ │ └── wizard_intro.go │ │ ├── sources/ │ │ │ ├── circleci/ │ │ │ │ └── circleci.go │ │ │ ├── docker/ │ │ │ │ └── docker.go │ │ │ ├── elasticsearch/ │ │ │ │ └── elasticsearch.go │ │ │ ├── filesystem/ │ │ │ │ └── filesystem.go │ │ │ ├── gcs/ │ │ │ │ └── gcs.go │ │ │ ├── git/ │ │ │ │ └── git.go │ │ │ ├── github/ │ │ │ │ └── github.go │ │ │ ├── gitlab/ │ │ │ │ └── gitlab.go │ │ │ ├── huggingface/ │ │ │ │ └── huggingface.go │ │ │ ├── jenkins/ │ │ │ │ └── jenkins.go │ │ │ ├── postman/ │ │ │ │ └── postman.go │ │ │ ├── s3/ │ │ │ │ └── s3.go │ │ │ ├── sources.go │ │ │ └── syslog/ │ │ │ └── syslog.go │ │ ├── styles/ │ │ │ └── styles.go │ │ └── tui.go │ ├── updater/ │ │ └── updater.go │ ├── verificationcache/ │ │ ├── in_memory_metrics.go │ │ ├── metrics_reporter.go │ │ ├── result_cache.go │ │ ├── verification_cache.go │ │ └── verification_cache_test.go │ ├── version/ │ │ └── version.go │ └── writers/ │ ├── buffer_writer/ │ │ ├── bufferwriter.go │ │ ├── bufferwriter_test.go │ │ └── metrics.go │ └── buffered_file_writer/ │ ├── bufferedfilewriter.go │ ├── bufferedfilewriter_test.go │ └── metrics.go ├── proto/ │ ├── config.proto │ ├── credentials.proto │ ├── custom_detectors.proto │ ├── detectors.proto │ ├── source_metadata.proto │ └── sources.proto └── scripts/ ├── gen_proto.sh ├── install.sh ├── test-last-changed-detector.sh └── test_changed_detectors.sh ================================================ FILE CONTENTS ================================================ ================================================ FILE: .captain/config.yaml ================================================ test-suites: detectors: command: gotestsum --jsonfile tmp/go-test.json --raw-command -- go test -tags=detectors -timeout=15m -json -count=1 -vet=off ./pkg/detectors/... results: path: tmp/go-test.json output: print-summary: true ## No retries right now # retries: # attempts: 3 # command: gotestsum --raw-command --jsonfile tmp/go-test.json -- go test -tags=detectors -timeout=15m -json -count=1 -vet=off {{ package }} -run '{{ run }}' ================================================ FILE: .gitattributes ================================================ *.go text eol=lf *.md text eol=lf ================================================ FILE: .github/ISSUE_TEMPLATE/bug_report.md ================================================ --- name: Bug report about: Create a report to help us improve title: "" labels: bug, needs triage assignees: trufflesecurity/product-eng --- Please review the [Community Note](https://github.com/trufflesecurity/trufflehog/blob/main/.github/community_note.md) before submitting ### TruffleHog Version ### Trace Output ### Expected Behavior ### Actual Behavior ### Steps to Reproduce 1. Go to '...' 2. Click on '....' 3. Scroll down to '....' 4. See error ## Environment * OS: [e.g. iOS] * Version [e.g. 22] ## Additional Context ### References * #0000 ================================================ FILE: .github/ISSUE_TEMPLATE/feature_request.md ================================================ --- name: Feature request about: Suggest an idea for this project title: "" labels: enhancement, needs triage assignees: trufflesecurity/product-eng --- Please review the [Community Note](https://github.com/trufflesecurity/trufflehog/blob/main/.github/community_note.md) before submitting ## Description ### Preferred Solution ### Additional Context #### References ================================================ FILE: .github/PULL_REQUEST_TEMPLATE.md ================================================ ### Description: Explain the purpose of the PR. ### Checklist: * [ ] Tests passing (`make test-community`)? * [ ] Lint passing (`make lint` this requires [golangci-lint](https://golangci-lint.run/welcome/install/#local-installation))? ================================================ FILE: .github/community_note.md ================================================ ## Community Note Please vote on this issue by adding a 👍 reaction to the original issue to help the community and maintainers prioritize this request. Please do not leave "+1" or other comments that do not add relevant new information or questions, they generate extra noise for issue followers and do not help prioritize the request. If you are interested in working on this issue or have submitted a pull request, please leave a comment. ================================================ FILE: .github/renovate.json ================================================ { "$schema": "https://docs.renovatebot.com/renovate-schema.json", "extends": [ "config:base" ], "prConcurrentLimit": 3, "prHourlyLimit": 2 } ================================================ FILE: .github/workflows/README.md ================================================ # GitHub Workflows This directory contains GitHub Actions workflows for the TruffleHog repository. ## PR Approval Check (`pr-approval-check.yml`) This workflow enforces that at least one PR approver must be an **active** member of the `@trufflesecurity/product-eng` team or any of its child teams. ### How it works: 1. **Triggers**: The workflow runs on: - `pull_request_review` events when a review is submitted (`submitted` type) - `pull_request` events when a PR is opened, reopened, or synchronized (`opened`, `reopened`, `synchronize` types) 2. **Approval Check Process**: The workflow: - Fetches all reviews for the PR using the GitHub API - Filters for reviews with state `APPROVED` - Gets all child teams of `@trufflesecurity/product-eng` using `listChildInOrg` API - Checks if any approver is an **active** member (not pending) of either: - The parent `@trufflesecurity/product-eng` team, OR - Any of its child teams - Sets a commit status accordingly 3. **Status Check**: Creates a commit status named `product-eng-approval` with: - ✅ **Success**: When at least one approver is an active member of `@trufflesecurity/product-eng` or any child team - ❌ **Failure**: When there are no approvals or there are approvals but none from active `@trufflesecurity/product-eng` members ### Error Handling If there are errors listing reviews or checking team membership, the workflow reports a failure status and also fails itself. ### Branch Protection To make this check required: 1. Go to Settings → Branches 2. Add or edit a branch protection rule for your main branch 3. Enable "Require status checks to pass before merging" 4. Add `pr-approval-check` to the required status checks ### Permissions The workflow uses the default `GITHUB_TOKEN` which has sufficient permissions to: - Read PR reviews - List child teams and check team membership (for public teams) - Create commit statuses **Note**: If the `product-eng` team or its child teams are private, you may need to use a personal access token with appropriate permissions. The Github API returns 404 for non-members and for lack of permissions. ================================================ FILE: .github/workflows/TESTING.md ================================================ # Testing Most testing is handled automatically by our GitHub Actions workflows. ## Local GitHub Action Testing In some cases you may wish to submit changes to the Trufflehog GitHub Action. Unfortunately GitHub does not provide a 1st-party testing environment for testing actions outside of GitHub Actions. Fortunately [nektos/act](https://github.com/nektos/act) enables local testing of GitHub Actions. ### Instructions 1. Please follow [the installation instructions](http://https://github.com/nektos/act#installation) for your OS. 2. The first run of `act` will ask you to specify an image. `Medium` should suffice. 3. You'll need to configure a personal-access-token(PAT) with: `repo:status`, `repo_deployment`, and `public_repo` permissions. 4. Set an environment variable named `GITHUB_TOKEN` with the PAT from the previous step as the value: `$ export GITHUB_TOKEN=` 5. Run the following command from the repository root: `act pull_request -j test -W .github/workflows/secrets.yml -s GITHUB_TOKEN --defaultbranch main` 6. If the job was successful, you should expect to see output from the scanner showing several detected secrets. 7. If you want to omit the context of a pull request event and just test that the action starts successfully, run: `act -j test -W .github/workflows/secrets.yml -s GITHUB_TOKEN --defaultbranch main` ================================================ FILE: .github/workflows/codeql-analysis.yml ================================================ # For most projects, this workflow file will not need changing; you simply need # to commit it to your repository. # # You may wish to alter this file to override the set of languages analyzed, # or to provide custom queries or build logic. # # ******** NOTE ******** # We have attempted to detect the languages in your repository. Please check # the `language` matrix defined below to confirm you have the correct set of # supported CodeQL languages. # name: "CodeQL" on: push: branches: [main] pull_request: # The branches below must be a subset of the branches above branches: [main] schedule: - cron: "35 11 * * 2" jobs: analyze: name: Analyze runs-on: ubuntu-latest permissions: actions: read contents: read security-events: write strategy: fail-fast: false matrix: language: ["go"] # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] # Learn more about CodeQL language support at https://git.io/codeql-language-support steps: - name: Checkout repository uses: actions/checkout@v4 - name: Install Go uses: actions/setup-go@v5 with: go-version: "1.24" # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL uses: github/codeql-action/init@v3 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. # By default, queries listed here will override any specified in a config file. # Prefix the list here with "+" to use these queries and those in the config file. # queries: ./path/to/local/query, your-org/your-repo/queries@main - name: Smoke run: | go build . - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@v3 ================================================ FILE: .github/workflows/detector-tests.yml ================================================ name: Detectors Aggregation on: workflow_dispatch: schedule: - cron: "0 8 * * *" jobs: test-detectors: if: ${{ github.repository == 'trufflesecurity/trufflehog' }} runs-on: ubuntu-latest permissions: actions: "read" contents: "read" id-token: "write" steps: - uses: actions/checkout@v4 - uses: actions/setup-go@v5 - name: Install gotestsum uses: jaxxstorm/action-install-gh-release@v1.14.0 with: repo: gotestyourself/gotestsum - uses: rwx-research/setup-captain@v1 - name: Test Go run: | export CGO_ENABLED=1 captain run detectors env: RWX_ACCESS_TOKEN: ${{ secrets.RWX_ACCESS_TOKEN }} ================================================ FILE: .github/workflows/lint.yml ================================================ name: Lint on: push: tags: - v* pull_request: permissions: contents: read pull-requests: read jobs: golangci-lint: name: golangci-lint runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-go@v5 with: go-version: "1.24" - name: golangci-lint uses: golangci/golangci-lint-action@v6 with: # Optional: version of golangci-lint to use in form of v1.2 or v1.2.3 or `latest` to use the latest version version: latest # Optional: working directory, useful for monorepos # working-directory: somedir # Optional: golangci-lint command line arguments. args: --enable bodyclose --enable copyloopvar --enable misspell --timeout 10m # Optional: if set to true then the action don't cache or restore ~/go/pkg. # skip-pkg-cache: true # Optional: if set to true then the action don't cache or restore ~/.cache/go-build. # skip-build-cache: true semgrep: name: semgrep runs-on: ubuntu-latest container: image: returntocorp/semgrep if: (github.actor != 'dependabot[bot]') steps: - uses: actions/checkout@v4 - run: semgrep --config=hack/semgrep-rules/detectors.yaml pkg/detectors/ ================================================ FILE: .github/workflows/performance.yml ================================================ name: Performance Test on: [pull_request] jobs: speed: # skip if PR is from a fork. # TODO: this could probabaly be refactored a bit so that it runs on forks if: ${{ ! github.event.pull_request.head.repo.fork }} runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v4 with: fetch-depth: 0 ref: ${{ github.head_ref }} - name: Install Go uses: actions/setup-go@v5 with: go-version: "1.24" - name: Run Head run: | go build -o current . repo_tmp=$(mktemp -d) git clone https://github.com/trufflesecurity/trufflehog.git $repo_tmp cd $repo_tmp git checkout v3.75.1 user_time_sum=0 for i in {1..5} do tmpfile=$(mktemp) /usr/bin/time -o $tmpfile $GITHUB_WORKSPACE/current filesystem "$repo_tmp" --no-verification --no-update > out.txt cat $tmpfile time_output=$(cat $tmpfile) rm $tmpfile user_time=$(echo $time_output | awk '{print $1}' | sed 's/user//') # Add the user time to the sum user_time_sum=$(echo "$user_time_sum + $user_time" | bc) done average_user_time=$(echo "scale=3; $user_time_sum / 5" | bc) echo HEAD_TIME=$average_user_time >> $GITHUB_ENV - name: Figure out previous tag run: | git fetch --tags git tag -l --sort=-v:refname | head -n 1 > previous_tag.txt echo PREVIOUS_TAG=$(cat previous_tag.txt) >> $GITHUB_ENV - name: Checkout code uses: actions/checkout@v4 with: fetch-depth: 0 ref: ${{ env.PREVIOUS_TAG }} - name: Run Previous run: | go build -o previous . repo_tmp=$(mktemp -d) git clone https://github.com/trufflesecurity/trufflehog.git $repo_tmp cd $repo_tmp git checkout v3.75.1 user_time_sum=0 for i in {1..5} do tmpfile=$(mktemp) /usr/bin/time -o $tmpfile $GITHUB_WORKSPACE/previous filesystem "$repo_tmp" --no-verification --no-update > out.txt cat $tmpfile time_output=$(cat $tmpfile) rm $tmpfile user_time=$(echo $time_output | awk '{print $1}' | sed 's/user//') # Add the user time to the sum user_time_sum=$(echo "$user_time_sum + $user_time" | bc) done average_user_time=$(echo "scale=3; $user_time_sum / 5" | bc) echo PREVIOUS_TIME=$average_user_time >> $GITHUB_ENV - name: Compare Results run: | echo "head ($GITHUB_SHA) avg time (n=5): $HEAD_TIME" echo "$PREVIOUS_TAG avg time (n=5): $PREVIOUS_TIME" if [ $(echo "$HEAD_TIME > $PREVIOUS_TIME * 1.5" | bc) -eq 1 ] then echo "HEAD run time is at least 10% slower than PREVIOUS run time" exit 1 fi ================================================ FILE: .github/workflows/release-guard.yml ================================================ name: Release Guard on: release: types: [created] permissions: contents: write jobs: unset-latest: runs-on: ubuntu-latest steps: - name: Restore previous release as latest if needed run: | LATEST_TAG=$(gh release list --json tagName,isLatest -q '.[] | select(.isLatest) | .tagName') if [ "$LATEST_TAG" != "${{ github.event.release.tag_name }}" ]; then echo "Release is not marked as latest (latest is $LATEST_TAG), skipping." exit 0 fi echo "Release ${{ github.event.release.tag_name }} is marked as latest, finding previous release..." # Get the second release in the list (sorted by date, excluding drafts/prereleases by default) # The first one is the current release, so we want the second one PREVIOUS_TAG=$(gh release list --exclude-drafts --exclude-pre-releases --json tagName -q '.[1].tagName') if [ -z "$PREVIOUS_TAG" ]; then echo "No previous release found, cannot restore. Exiting." exit 0 fi echo "Restoring $PREVIOUS_TAG as latest..." gh release edit "$PREVIOUS_TAG" --latest env: GH_TOKEN: ${{ github.token }} ================================================ FILE: .github/workflows/release.yml ================================================ name: Release on: push: tags: - v* permissions: contents: write packages: write id-token: write jobs: Release: runs-on: ubuntu-latest env: DOCKER_CLI_EXPERIMENTAL: "enabled" steps: - name: Checkout uses: actions/checkout@v4 with: fetch-depth: 0 - name: Set up QEMU uses: docker/setup-qemu-action@v3 - name: Docker Login to DockerHub uses: docker/login-action@v3 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Docker Login to GitHub Container Registry uses: docker/login-action@v3 with: registry: ghcr.io username: ${{ github.repository_owner }} password: ${{ secrets.GITHUB_TOKEN }} - name: Set up Go uses: actions/setup-go@v5 with: go-version: "1.24" - name: Cosign install uses: sigstore/cosign-installer@d58896d6a1865668819e1d91763c7751a165e159 # v3.9.2 - name: Install UPX run: | sudo apt-get update sudo apt-get install -y upx - name: Run GoReleaser uses: goreleaser/goreleaser-action@v6 with: distribution: goreleaser-pro version: latest args: release --clean env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} HOMEBREW_TAP_TOKEN: ${{ secrets.HOMEBREW_TAP_TOKEN }} GORELEASER_KEY: ${{ secrets.GORELEASER_KEY }} - name: Mark release as latest run: gh release edit ${{ github.ref_name }} --latest env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} ================================================ FILE: .github/workflows/secrets.yml ================================================ name: Scan for secrets on: push: tags: - v* branches: - main pull_request: workflow_dispatch: jobs: test: if: ${{ github.repository == 'trufflesecurity/trufflehog' && !github.event.pull_request.head.repo.fork }} runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v4 with: fetch-depth: 0 ref: ${{ github.head_ref }} - name: Dogfood uses: ./ id: dogfood with: extra_args: --results=verified ================================================ FILE: .github/workflows/smoke.yml ================================================ name: Smoke on: pull_request: jobs: smoke: runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v4 - name: Install Go uses: actions/setup-go@v5 with: go-version: "1.24" - name: Smoke run: | set -e go run . git https://github.com/dustin-decker/secretsandstuff.git > /dev/null go run . github --repo https://github.com/dustin-decker/secretsandstuff.git > /dev/null zombies: runs-on: ubuntu-latest timeout-minutes: 5 steps: - name: Checkout code uses: actions/checkout@v4 - name: Install Go uses: actions/setup-go@v5 with: go-version: "1.24" - name: Run trufflehog run: | set -e go run . git --no-verification file://. > /dev/null # This case previously had a deadlock issue and left zombies after trufflehog exited #3379 go run . git --no-verification https://github.com/git-test-fixtures/binary.git > /dev/null - name: Check for running git processes and zombies run: | if pgrep -x "git" > /dev/null then echo "Git processes are still running" exit 1 else echo "No git processes found" fi if ps -A -ostat,ppid | grep -e '[zZ]' > /dev/null then echo "Zombie processes found" exit 1 else echo "No zombie processes found" fi ================================================ FILE: .github/workflows/test.yml ================================================ name: Test on: push: tags: - v* branches: - main pull_request: jobs: test: if: ${{ github.repository == 'trufflesecurity/trufflehog' && !github.event.pull_request.head.repo.fork }} runs-on: ubuntu-latest permissions: actions: "read" contents: "read" id-token: "write" steps: - name: Checkout code uses: actions/checkout@v4 - name: Install Go uses: actions/setup-go@v5 with: go-version: "1.24" - id: "auth" uses: "google-github-actions/auth@v2" with: workload_identity_provider: "projects/811013774421/locations/global/workloadIdentityPools/github-pool/providers/github-provider" service_account: "github-ci-external@trufflehog-testing.iam.gserviceaccount.com" - name: Set up gotestsum run: | go install gotest.tools/gotestsum@latest mkdir -p tmp/test-results - name: Test run: | CGO_ENABLED=1 gotestsum --junitfile tmp/test-results/test.xml --raw-command -- go test -json -tags=sources $(go list ./... | grep -v /vendor/ | grep -v pkg/analyzer/analyzers) if: ${{ success() || failure() }} # always run this step, even if there were previous errors - name: Upload test results to BuildPulse for flaky test detection if: ${{ !cancelled() }} # Run this step even when the tests fail. Skip if the workflow is cancelled. uses: buildpulse/buildpulse-action@main with: account: 79229934 repository: 77726177 path: | tmp/test-results/*.xml key: ${{ secrets.BUILDPULSE_ACCESS_KEY_ID }} secret: ${{ secrets.BUILDPULSE_SECRET_ACCESS_KEY }} tags: integration - name: Annotate test results uses: mikepenz/action-junit-report@v5 if: success() || failure() # always run even if the previous step fails with: report_paths: "tmp/test-results/*.xml" test-community: if: ${{ github.event.pull_request.head.repo.fork }} runs-on: ubuntu-latest permissions: actions: "read" contents: "read" steps: - name: Checkout code uses: actions/checkout@v4 - name: Install Go uses: actions/setup-go@v5 with: go-version: "1.24" - name: Test run: make test-community ================================================ FILE: .gitignore ================================================ .idea dist .env *.test # binary trufflehog tmp/go-test.json .captain/detectors/timings.yaml .captain/detectors/quarantines.yaml .captain/detectors/flakes.yaml .vscode ================================================ FILE: .goreleaser.yml ================================================ version: 2 release: make_latest: false builds: - id: trufflehog-upx binary: trufflehog ldflags: - -s -w -X 'github.com/trufflesecurity/trufflehog/v3/pkg/version.BuildVersion={{ .Version }}' env: [CGO_ENABLED=0] goos: - linux goarch: - amd64 - arm64 hooks: post: - upx -q "{{ .Path }}" - id: trufflehog binary: trufflehog ldflags: - -X 'github.com/trufflesecurity/trufflehog/v3/pkg/version.BuildVersion={{ .Version }}' env: [CGO_ENABLED=0] goos: - darwin - windows goarch: - amd64 - arm64 dockers: - image_templates: ["trufflesecurity/{{ .ProjectName }}:{{ .Version }}-amd64"] dockerfile: Dockerfile.goreleaser extra_files: - entrypoint.sh use: buildx build_flag_templates: - --platform=linux/amd64 - --label=org.opencontainers.image.title={{ .ProjectName }} - --label=org.opencontainers.image.description={{ .ProjectName }} - --label=org.opencontainers.image.url=https://github.com/trufflesecurity/{{ .ProjectName }} - --label=org.opencontainers.image.source=https://github.com/trufflesecurity/{{ .ProjectName }} - --label=org.opencontainers.image.version={{ .Version }} - --label=org.opencontainers.image.revision={{ .FullCommit }} - --label=org.opencontainers.image.licenses=AGPL-3.0 - --provenance=false - image_templates: ["trufflesecurity/{{ .ProjectName }}:{{ .Version }}-arm64v8"] goarch: arm64 dockerfile: Dockerfile.goreleaser extra_files: - entrypoint.sh use: buildx build_flag_templates: - --platform=linux/arm64/v8 - --label=org.opencontainers.image.title={{ .ProjectName }} - --label=org.opencontainers.image.description={{ .ProjectName }} - --label=org.opencontainers.image.url=https://github.com/trufflesecurity/{{ .ProjectName }} - --label=org.opencontainers.image.source=https://github.com/trufflesecurity/{{ .ProjectName }} - --label=org.opencontainers.image.version={{ .Version }} - --label=org.opencontainers.image.revision={{ .FullCommit }} - --label=org.opencontainers.image.licenses=AGPL-3.0 - --provenance=false - image_templates: ["ghcr.io/trufflesecurity/{{ .ProjectName }}:{{ .Version }}-amd64"] dockerfile: Dockerfile.goreleaser extra_files: - entrypoint.sh use: buildx build_flag_templates: - --platform=linux/amd64 - --label=org.opencontainers.image.title={{ .ProjectName }} - --label=org.opencontainers.image.description={{ .ProjectName }} - --label=org.opencontainers.image.url=https://github.com/trufflesecurity/{{ .ProjectName }} - --label=org.opencontainers.image.source=https://github.com/trufflesecurity/{{ .ProjectName }} - --label=org.opencontainers.image.version={{ .Version }} - --label=org.opencontainers.image.revision={{ .FullCommit }} - --label=org.opencontainers.image.licenses=AGPL-3.0 - --provenance=false - image_templates: ["ghcr.io/trufflesecurity/{{ .ProjectName }}:{{ .Version }}-arm64v8"] goarch: arm64 dockerfile: Dockerfile.goreleaser extra_files: - entrypoint.sh use: buildx build_flag_templates: - --platform=linux/arm64/v8 - --label=org.opencontainers.image.title={{ .ProjectName }} - --label=org.opencontainers.image.description={{ .ProjectName }} - --label=org.opencontainers.image.url=https://github.com/trufflesecurity/{{ .ProjectName }} - --label=org.opencontainers.image.source=https://github.com/trufflesecurity/{{ .ProjectName }} - --label=org.opencontainers.image.version={{ .Version }} - --label=org.opencontainers.image.revision={{ .FullCommit }} - --label=org.opencontainers.image.licenses=AGPL-3.0 - --provenance=false docker_manifests: - name_template: trufflesecurity/{{ .ProjectName }}:{{ .Version }} image_templates: - trufflesecurity/{{ .ProjectName }}:{{ .Version }}-amd64 - trufflesecurity/{{ .ProjectName }}:{{ .Version }}-arm64v8 - name_template: trufflesecurity/{{ .ProjectName }}:latest image_templates: - trufflesecurity/{{ .ProjectName }}:{{ .Version }}-amd64 - trufflesecurity/{{ .ProjectName }}:{{ .Version }}-arm64v8 - name_template: ghcr.io/trufflesecurity/{{ .ProjectName }}:{{ .Version }} image_templates: - ghcr.io/trufflesecurity/{{ .ProjectName }}:{{ .Version }}-amd64 - ghcr.io/trufflesecurity/{{ .ProjectName }}:{{ .Version }}-arm64v8 - name_template: ghcr.io/trufflesecurity/{{ .ProjectName }}:latest image_templates: - ghcr.io/trufflesecurity/{{ .ProjectName }}:{{ .Version }}-amd64 - ghcr.io/trufflesecurity/{{ .ProjectName }}:{{ .Version }}-arm64v8 brews: - repository: owner: trufflesecurity name: homebrew-trufflehog token: "{{ .Env.HOMEBREW_TAP_TOKEN }}" description: "Find credentials all over the place" name: "trufflehog" homepage: "https://github.com/trufflesecurity/trufflehog" install: | bin.install "trufflehog" signs: - cmd: cosign signature: "${artifact}.sig" certificate: "${artifact}.pem" args: - "sign-blob" - "--oidc-issuer=https://token.actions.githubusercontent.com" - "--output-certificate=${certificate}" - "--output-signature=${signature}" - "${artifact}" - "--yes" artifacts: checksum ================================================ FILE: .pre-commit-config.yaml ================================================ repos: - repo: https://github.com/rhysd/actionlint rev: v1.6.24 hooks: - id: actionlint - repo: https://github.com/mpalmer/action-validator rev: v0.5.1 hooks: - id: action-validator ================================================ FILE: .pre-commit-hooks.yaml ================================================ - id: trufflehog name: TruffleHog description: Detect secrets in your data with TruffleHog. entry: trufflehog git file://. --since-commit HEAD --results=verified --fail --trust-local-git-config language: golang pass_filenames: false ================================================ FILE: CODEOWNERS ================================================ # catch-all * @trufflesecurity/product-eng # Scanning pkg/sources/ @trufflesecurity/Scanning pkg/writers/ @trufflesecurity/Scanning # Integrations pkg/sources/circleci/ @trufflesecurity/Integrations pkg/sources/docker/ @trufflesecurity/Integrations pkg/sources/elasticsearch/ @trufflesecurity/Integrations pkg/sources/filesystem/ @trufflesecurity/Integrations pkg/sources/gcs/ @trufflesecurity/Integrations pkg/sources/git/ @trufflesecurity/Integrations pkg/sources/github/ @trufflesecurity/Integrations pkg/sources/gitlab/ @trufflesecurity/Integrations pkg/sources/jenkins/ @trufflesecurity/Integrations pkg/sources/postman/ @trufflesecurity/Integrations pkg/sources/s3/ @trufflesecurity/Integrations pkg/sources/travisci/ @trufflesecurity/Integrations # Shared pkg/decoders/ @trufflesecurity/Scanning @trufflesecurity/OSS pkg/engine/ @trufflesecurity/Scanning @trufflesecurity/OSS pkg/gitparse/ @trufflesecurity/Scanning @trufflesecurity/OSS pkg/giturl/ @trufflesecurity/Scanning @trufflesecurity/OSS pkg/handlers/ @trufflesecurity/Scanning @trufflesecurity/OSS pkg/iobuf/ @trufflesecurity/Scanning @trufflesecurity/OSS pkg/sanitizer/ @trufflesecurity/Scanning @trufflesecurity/OSS proto/ @trufflesecurity/Scanning @trufflesecurity/Integrations # OSS pkg/detectors/ @trufflesecurity/OSS pkg/common/ @trufflesecurity/OSS pkg/custom_detectors/ @trufflesecurity/OSS pkg/analyzer/ @trufflesecurity/OSS pkg/engine/defaults/defaults.go @trufflesecurity/OSS pkg/engine/defaults/defaults_test.go @trufflesecurity/OSS # critical detectors pkg/detectors/aws/ @trufflesecurity/backend pkg/detectors/gcp/ @trufflesecurity/backend pkg/detectors/azure/ @trufflesecurity/backend pkg/detectors/okta/ @trufflesecurity/backend pkg/detectors/privatekey/ @trufflesecurity/backend pkg/detectors/slack/ @trufflesecurity/backend pkg/detectors/slackwebhook/ @trufflesecurity/backend pkg/detectors/microsoftteamswebhook/ @trufflesecurity/backend pkg/detectors/twilio/ @trufflesecurity/backend pkg/detectors/sendgrid/ @trufflesecurity/backend pkg/detectors/gitlab/ @trufflesecurity/backend pkg/detectors/gitlabv2/ @trufflesecurity/backend pkg/detectors/github/ @trufflesecurity/backend pkg/detectors/github_old/ @trufflesecurity/backend pkg/detectors/githubapp/ @trufflesecurity/backend ================================================ FILE: CODE_OF_CONDUCT.md ================================================ # Contributor Covenant Code of Conduct ## Our Pledge In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. ## Our Standards Examples of behavior that contributes to creating a positive environment include: * Using welcoming and inclusive language * Being respectful of differing viewpoints and experiences * Gracefully accepting constructive criticism * Focusing on what is best for the community * Showing empathy towards other community members Examples of unacceptable behavior by participants include: * The use of sexualized language or imagery and unwelcome sexual attention or advances * Trolling, insulting/derogatory comments, and personal or political attacks * Public or private harassment * Publishing others' private information, such as a physical or electronic address, without explicit permission * Other conduct which could reasonably be considered inappropriate in a professional setting ## Our Responsibilities Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. ## Scope This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at community@trufflesec.com. All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. ## Attribution This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html [homepage]: https://www.contributor-covenant.org For answers to common questions about this code of conduct, see https://www.contributor-covenant.org/faq ================================================ FILE: CONTRIBUTING.md ================================================ # Contribution guidelines Please create an issue to collect feedback prior to feature additions. If possible try to keep PRs scoped to one feature, and add tests for new features. We use the fork-based contribution model described by [GitHub's documentation](https://docs.github.com/en/get-started/exploring-projects-on-github/contributing-to-a-project). (In short: Fork the TruffleHog repo and open a PR back from your fork into our default branch.) When showing interest in a bug, enhancement, PR, or issue, please use the thumbs up/thumbs down emoji on the original message rather than adding comments expressing the same. Contributors need to [sign our CLA](https://cla-assistant.io/trufflesecurity/trufflehog) before we are able to accept contributions. # Resources ## How things work It can be a bit daunting diving into the code and wrapping your head around the project from a high level. The following two docs help give that high level overview: * [Process Flow](docs/process_flow.md) * [Concurrency Overview](docs/concurrency.md) ## 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! ## Logging in TruffleHog **Use fields over format strings**. For structured logging, fields allow us to better filter and search through logs than embedding data in the message. **Differentiate logs coming from dependencies**. This can be done with a `"dep"` field that gets passed to the library. Sometimes it’s not possible to do this. Limit log levels to _**info**_ (indicate normal or expected operation) and _**error**_ (functionality is impeded and should be checked by an engineer) **Choose an appropriate verbosity level** ``` 0. — logs we always want to see 1. — logs we could possibly want to turn off 2. — logs that are useful for debugging 3. — frequently called logs that may produce a lot of output 4. — extremely verbose logs or logs containing sensitive information 5. — ultimate verbosity ``` Example: `Logger().V(2).Info("skipping file: extension is ignored", "ext", mimeExt)` **Either log an error or return it**. Doing one or the other will help defer logging for when there is more context for it and prevent duplicate “bubbling up” logs. **Log contextual information**. Every log emitted should contain this context via fields to easily filter and search. ================================================ FILE: Dockerfile ================================================ FROM --platform=${BUILDPLATFORM} golang:bullseye as builder WORKDIR /build COPY . . ENV CGO_ENABLED=0 ARG TARGETOS TARGETARCH RUN --mount=type=cache,target=/go/pkg/mod \ --mount=type=cache,target=/root/.cache/go-build \ GOOS=${TARGETOS} GOARCH=${TARGETARCH} go build -o trufflehog . FROM alpine:3.22 RUN apk add --no-cache bash git openssh-client ca-certificates rpm2cpio binutils cpio \ && rm -rf /var/cache/apk/* && update-ca-certificates COPY --from=builder /build/trufflehog /usr/bin/trufflehog COPY entrypoint.sh /etc/entrypoint.sh RUN chmod +x /etc/entrypoint.sh ENTRYPOINT ["/etc/entrypoint.sh"] ================================================ FILE: Dockerfile.goreleaser ================================================ FROM alpine:3.22 RUN apk add --no-cache bash git openssh-client ca-certificates \ && rm -rf /var/cache/apk/* && update-ca-certificates WORKDIR /usr/bin/ COPY trufflehog . COPY entrypoint.sh /etc/entrypoint.sh RUN chmod +x /etc/entrypoint.sh ENTRYPOINT ["/etc/entrypoint.sh"] ================================================ FILE: LICENSE ================================================ GNU AFFERO GENERAL PUBLIC LICENSE Version 3, 19 November 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The GNU Affero General Public License is a free, copyleft license for software and other kinds of works, specifically designed to ensure cooperation with the community in the case of network server software. The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, our General Public Licenses are intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. Developers that use our General Public Licenses protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License which gives you legal permission to copy, distribute and/or modify the software. A secondary benefit of defending all users' freedom is that improvements made in alternate versions of the program, if they receive widespread use, become available for other developers to incorporate. Many developers of free software are heartened and encouraged by the resulting cooperation. However, in the case of software used on network servers, this result may fail to come about. The GNU General Public License permits making a modified version and letting the public access it on a server without ever releasing its source code to the public. The GNU Affero General Public License is designed specifically to ensure that, in such cases, the modified source code becomes available to the community. It requires the operator of a network server to provide the source code of the modified version running there to the users of that server. Therefore, public use of a modified version, on a publicly accessible server, gives the public access to the source code of the modified version. An older license, called the Affero General Public License and published by Affero, was designed to accomplish similar goals. This is a different license, not a version of the Affero GPL, but Affero has released a new version of the Affero GPL which permits relicensing under this license. The precise terms and conditions for copying, distribution and modification follow. TERMS AND CONDITIONS 0. Definitions. "This License" refers to version 3 of the GNU Affero General Public License. "Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. "The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations. To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work. A "covered work" means either the unmodified Program or a work based on the Program. To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. 1. Source Code. The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work. A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work. The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. The Corresponding Source for a work in source code form is that same work. 2. Basic Permissions. All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. 3. Protecting Users' Legal Rights From Anti-Circumvention Law. No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. 4. Conveying Verbatim Copies. You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. 5. Conveying Modified Source Versions. You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: a) The work must carry prominent notices stating that you modified it, and giving a relevant date. b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices". c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. 6. Conveying Non-Source Forms. You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. "Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. 7. Additional Terms. "Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or d) Limiting the use for publicity purposes of names of licensors or authors of the material; or e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. 8. Termination. You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. 9. Acceptance Not Required for Having Copies. You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. 10. Automatic Licensing of Downstream Recipients. Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. 11. Patents. A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version". A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. 12. No Surrender of Others' Freedom. If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. 13. Remote Network Interaction; Use with the GNU General Public License. Notwithstanding any other provision of this License, if you modify the Program, your modified version must prominently offer all users interacting with it remotely through a computer network (if your version supports such interaction) an opportunity to receive the Corresponding Source of your version by providing access to the Corresponding Source from a network server at no charge, through some standard or customary means of facilitating copying of software. This Corresponding Source shall include the Corresponding Source for any work covered by version 3 of the GNU General Public License that is incorporated pursuant to the following paragraph. Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the work with which it is combined will remain governed by version 3 of the GNU General Public License. 14. Revised Versions of this License. The Free Software Foundation may publish revised and/or new versions of the GNU Affero General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU Affero General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU Affero General Public License, you may choose any version ever published by the Free Software Foundation. If the Program specifies that a proxy can decide which future versions of the GNU Affero General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. 15. Disclaimer of Warranty. THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. Limitation of Liability. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 17. Interpretation of Sections 15 and 16. If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . Also add information on how to contact you by electronic and paper mail. If your software can interact with users remotely through a computer network, you should also make sure that it provides a way for users to get its source. For example, if your program is a web application, its interface could display a "Source" link that leads users to an archive of the code. There are many ways you could offer source, and different solutions will be better for different programs; see section 13 for the specific requirements. You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. For more information on this, and how to apply and follow the GNU AGPL, see . Copy license text to clipboard Suggest this license Make a pull request to suggest this license for a project that is not licensed. Please be polite: see if a license has already been suggested, try to suggest a license fitting for the project’s community, and keep your communication with project maintainers friendly. Enter GitHub repository URL How to apply this license Create a text file (typically named LICENSE or LICENSE.txt) in the root of your source code and copy the text of the license into the file. Optional steps The Free Software Foundation recommends taking the additional step of adding a boilerplate notice to the top of each file. The boilerplate can be found at the end of the license. Add AGPL-3.0-or-later (or AGPL-3.0-only to disallow future versions) to your project’s package description, if applicable (e.g., Node.js, Ruby, and Rust). This will ensure the license is displayed in package directories. Source ================================================ FILE: Makefile ================================================ PROTOS_IMAGE ?= trufflesecurity/protos:1.22 .PHONY: check .PHONY: lint .PHONY: test .PHONY: test-race .PHONY: run .PHONY: install .PHONY: protos .PHONY: protos-windows .PHONY: vendor .PHONY: dogfood dogfood: CGO_ENABLED=0 go run . git file://. --json --log-level=2 install: CGO_ENABLED=0 go install . check: go fmt $(shell go list ./... | grep -v /vendor/) go vet $(shell go list ./... | grep -v /vendor/) lint: golangci-lint run --enable bodyclose --enable copyloopvar --enable misspell --out-format=colored-line-number --timeout 10m test-failing: CGO_ENABLED=0 go test -timeout=5m $(shell go list ./... | grep -v /vendor/) | grep FAIL test: CGO_ENABLED=0 go test -timeout=5m $(shell go list ./... | grep -v /vendor/) test-integration: CGO_ENABLED=0 go test -timeout=5m -tags=integration $(shell go list ./... | grep -v /vendor/) test-race: CGO_ENABLED=1 go test -timeout=5m -race $(shell go list ./... | grep -v /vendor/) test-detectors: CGO_ENABLED=0 go test -tags=detectors -timeout=5m $(shell go list ./... | grep pkg/detectors) test-community: CGO_ENABLED=0 go test -timeout=5m $(shell go list ./... | grep -v /vendor/ | grep -v pkg/sources | grep -v pkg/analyzer/analyzers) bench: CGO_ENABLED=0 go test $(shell go list ./pkg/secrets/... | grep -v /vendor/) -benchmem -run=xxx -bench . run: CGO_ENABLED=0 go run . git file://. --json run-debug: CGO_ENABLED=0 go run . git file://. --json --log-level=2 protos: docker run --rm -u "$(shell id -u)" -v "$(shell dirname $(realpath $(firstword $(MAKEFILE_LIST))))":/pwd "${PROTOS_IMAGE}" bash -c "cd /pwd; /pwd/scripts/gen_proto.sh" protos-windows: docker run --rm -v "$(shell cygpath -w $(shell pwd))":/pwd "${PROTOS_IMAGE}" bash -c "cd /pwd; ./scripts/gen_proto.sh" release-protos-image: docker buildx build --push --platform=linux/amd64,linux/arm64 \ -t ${PROTOS_IMAGE} -f hack/Dockerfile.protos . test-release: goreleaser release --clean --skip-publish --snapshot ================================================ FILE: PreCommit.md ================================================ # TruffleHog Pre-Commit Hooks Pre-commit hooks are scripts that run automatically before a commit is completed, allowing you to check your code for issues before sharing it with others. TruffleHog can be integrated as a pre-commit hook to prevent credentials from leaking before they ever leave your computer. This guide covers how to set up TruffleHog as a pre-commit hook using two popular frameworks: 1. [Git's hooksPath feature](#global-setup-using-gits-hookspath-feature) - A built-in Git feature for managing hooks globally 2. [Using Pre-commit framework](#using-the-pre-commit-framework) - A language-agnostic framework for managing pre-commit hooks 3. [Using Husky](#using-husky) - A Git hooks manager for JavaScript/Node.js projects ## Prerequisites All of the methods require TruffleHog to be installed. 1. Install TruffleHog: ```bash # Using Homebrew (macOS) brew install trufflehog # Using installation script for Linux, macOS, and Windows (and WSL) curl -sSfL https://raw.githubusercontent.com/trufflesecurity/trufflehog/main/scripts/install.sh | sh -s -- -b /usr/local/bin ``` ## Global setup using Git's hooksPath feature This approach uses Git's `core.hooksPath` to apply hooks to all repositories without requiring any per-repository setup: 1. Create a global hooks directory: ```bash mkdir -p ~/.git-hooks ``` 2. Create a pre-commit hook file: ```bash touch ~/.git-hooks/pre-commit chmod +x ~/.git-hooks/pre-commit ``` 3. Configure Git Hook Script ### **Standard Installation** #### **Option A: Auto-configured (Recommended)** TruffleHog automatically detects the `TRUFFLEHOG_PRE_COMMIT` environment variable and applies optimal pre-commit settings. ```bash #!/bin/sh export TRUFFLEHOG_PRE_COMMIT=1 trufflehog git file://. ``` #### **Option B: Manual-configuration** Manual configuration (only if you need custom behavior). Do NOT set `TRUFFLEHOG_PRE_COMMIT` if using manual configuration. ```bash #!bin/sh trufflehog git file://. --since-commit HEAD --results=verified,unknown --fail --trust-local-git-config ``` ### **Docker Installation** #### **Option A: Auto-configured (Recommended)** ```bash #!/bin/sh # Set environment variable inside container (recommended) docker run --rm \ -v "$(pwd):/workdir" \ -e "TRUFFLEHOG_PRE_COMMIT=1" \ trufflesecurity/trufflehog:latest \ git file:///workdir ``` #### **Option B: Manual-configuration** ```bash #!/bin/sh docker run --rm -v "$(pwd):/workdir" -i --rm trufflesecurity/trufflehog:latest git file:///workdir --since-commit HEAD --results=verified,unknown --fail ``` 4. Configure Git to use this hooks directory globally: ```bash git config --global core.hooksPath ~/.git-hooks ``` Now all your repositories will automatically use this pre-commit hook without any additional setup. ## Using the Pre-commit Framework The [pre-commit framework](https://pre-commit.com) is a powerful, language-agnostic tool for managing Git hooks. ### Installation of Pre-commit 1. Install the pre-commit framework: ```bash # Using pip (Python) pip install pre-commit # Using Homebrew (macOS) brew install pre-commit # Using conda conda install -c conda-forge pre-commit ``` ### Repository-Specific Setup To set up TruffleHog as a pre-commit hook for a specific repository: 1. Create a `.pre-commit-config.yaml` file in the root of your repository: TruffleHog automatically detects when running under the pre-commit.com framework and applies optimal settings. No additional configuration is needed. ```yaml repos: - repo: local hooks: - id: trufflehog name: TruffleHog description: Detect secrets in your data. entry: bash -c 'trufflehog git file://.' language: system stages: ["pre-commit", "pre-push"] ``` If TruffleHog doesn't auto-detect your pre-commit.com environment, you can manually specify the recommended pre-commit settings: ```yaml repos: - repo: local hooks: - id: trufflehog name: TruffleHog description: Detect secrets in your data. entry: bash -c 'trufflehog git file://. --since-commit HEAD --results=verified,unknown --fail --trust-local-git-config' language: system stages: ["pre-commit", "pre-push"] ``` 2. Install the pre-commit hook: ```bash pre-commit install ``` ## Using Husky [Husky](https://typicode.github.io/husky/) is a popular tool for managing Git hooks in JavaScript/Node.js projects. ### Installation of Husky 1. Install Husky in your project: ```bash # npm npm install husky --save-dev # yarn yarn add husky --dev ``` 2. Enable Git hooks: ```bash # npm npx husky init ``` ### Setting Up TruffleHog with Husky 1. Add the following content to `.husky/pre-commit`: TruffleHog automatically detects when running under the Husky framework and applies optimal settings. No additional configuration is needed. ```bash echo "trufflehog git file://." > .husky/pre-commit ``` If TruffleHog doesn't auto-detect your husky framework, you can manually specify the recommended pre-commit settings: ```bash echo "trufflehog git file://. --since-commit HEAD --results=verified,unknown --fail --trust-local-git-config" > .husky/pre-commit ``` 2. For Docker users, use this content instead: ```bash echo 'docker run --rm -v "$(pwd):/workdir" -i --rm trufflesecurity/trufflehog:latest git file:///workdir' > .husky/pre-commit ``` ## Best Practices ### Commit Process For optimal hook efficacy: 1. Execute `git add` followed by `git commit` separately. This ensures TruffleHog analyzes all intended changes. 2. Avoid using `git commit -am`, as it might bypass pre-commit hook execution for unstaged modifications. ### Skipping Hooks In rare cases, you may need to bypass pre-commit hooks: ```bash git commit --no-verify -m "Your commit message" ``` ### Running in Audit Mode (Without TRUFFLEHOG_PRE_COMMIT env variable) You can run the TruffleHog pre-commit hook in an "audit" or "non-enforcement" mode to test the git hook with the following commands: Local Binary Version: ```bash trufflehog git file://. --since-commit HEAD --results=verified,unknown 2>/dev/null ``` Docker Container Version: ```bash docker run --rm -v "$(pwd):/workdir" -i --rm trufflesecurity/trufflehog:latest git file:///workdir --since-commit HEAD --results=verified,unknown 2>/dev/null ``` This change does two things: (1) removes the `--fail` flag, which means the pre-commit hook will *always* pass, (2) suppresses `stderr` output, so only verified secrets are printed to the terminal output. **For users of the Pre-Commit Framework: add the `verbose: true` flag during audit mode; otherwise, the hook will pass, and you won't see any secrets.** ## Troubleshooting ### Hook Not Running If your pre-commit hook isn't running: 1. Ensure the hook is executable: ```bash chmod +x .git/hooks/pre-commit ``` 2. Check if hooks are enabled: ```bash git config --get core.hooksPath ``` ### False Positives If you're getting false positives: 1. Use the `--results=verified` flag to only show verified secrets 2. Add `trufflehog:ignore` comments on lines with known false positives or risk-accepted findings ## Conclusion By integrating TruffleHog into your pre-commit workflow, you can prevent credential leaks before they happen. Choose the setup method that best fits your project's needs and development workflow. For more information on TruffleHog's capabilities, refer to the [main documentation](README.md). ================================================ FILE: README.md ================================================

GoReleaser Logo

TruffleHog

Find leaked credentials.

---
[![Go Report Card](https://goreportcard.com/badge/github.com/trufflesecurity/trufflehog/v3)](https://goreportcard.com/report/github.com/trufflesecurity/trufflehog/v3) [![License](https://img.shields.io/badge/license-AGPL--3.0-brightgreen)](/LICENSE) [![Total Detectors](https://img.shields.io/github/directory-file-count/trufflesecurity/truffleHog/pkg/detectors?label=Total%20Detectors&type=dir)](/pkg/detectors)
--- # :mag_right: _Now Scanning_
**...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 ![GitHub scanning demo](https://storage.googleapis.com/truffle-demos/non-interactive.svg) ```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/). ![image](https://github.com/trufflesecurity/trufflehog/assets/52866392/74ace530-08c5-4eaf-a169-84a73e328f6f) ### 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|\\|$)`) // &#xX; format - HTML/XML (hex with semicolon) htmlEscapePat = regexp.MustCompile(`&#x([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 &#xX; 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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\""), }, }, // &#xX; format - HTML/XML { name: "[html] &#xX; 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 production 2023-05-18T14:32:10Z jenkins-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 production 2023-05-18T14:32:10Z jenkins-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 production 2023-05-18T14:32:10Z jenkins-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 production 2023-05-18T14:32:10Z jenkins-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 production 2023-05-18T14:32:10Z jenkins-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 production 2023-05-18T14:32:10Z jenkins-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 production 2023-05-18T14:32:10Z jenkins-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 production 2023-05-18T14:32:10Z jenkins-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 production 2023-05-18T14:32:10Z jenkins-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 production 2023-05-18T14:32:10Z jenkins-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 production 2023-05-18T14:32:10Z jenkins-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 production 2023-05-18T14:32:10Z jenkins-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 production 2023-05-18T14:32:10Z jenkins-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 production 2023-05-18T14:32:10Z jenkins-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 production 2023-05-18T14:32:10Z jenkins-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 production 2023-05-18T14:32:10Z jenkins-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 production 2023-05-18T14:32:10Z jenkins-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 production 2023-05-18T14:32:10Z jenkins-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 production 2023-05-18T14:32:10Z jenkins-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 production 2023-05-18T14:32:10Z jenkins-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 production 2023-05-18T14:32:10Z jenkins-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 production 2023-05-18T14:32:10Z jenkins-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 production 2023-05-18T14:32:10Z jenkins-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 production 2023-05-18T14:32:10Z jenkins-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 production 2023-05-18T14:32:10Z jenkins-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 production 2023-05-18T14:32:10Z jenkins-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 production 2023-05-18T14:32:10Z jenkins-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 production 2023-05-18T14:32:10Z jenkins-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 production 2023-05-18T14:32:10Z jenkins-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 production 2023-05-18T14:32:10Z jenkins-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 production 2023-05-18T14:32:10Z jenkins-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 production 2023-05-18T14:32:10Z jenkins-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 production 2023-05-18T14:32:10Z jenkins-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 production 2023-05-18T14:32:10Z jenkins-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 production 2023-05-18T14:32:10Z jenkins-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 production 2023-05-18T14:32:10Z jenkins-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 production 2023-05-18T14:32:10Z jenkins-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 production 2023-05-18T14:32:10Z jenkins-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 production 2023-05-18T14:32:10Z jenkins-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 production 2023-05-18T14:32:10Z jenkins-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 production 2023-05-18T14:32:10Z jenkins-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 production 2023-05-18T14:32:10Z jenkins-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 production 2023-05-18T14:32:10Z jenkins-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 production 2023-05-18T14:32:10Z jenkins-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 production 2023-05-18T14:32:10Z jenkins-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 production 2023-05-18T14:32:10Z jenkins-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 production 2023-05-18T14:32:10Z jenkins-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 production 2023-05-18T14:32:10Z jenkins-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 production 2023-05-18T14:32:10Z jenkins-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 production 2023-05-18T14:32:10Z jenkins-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 production 2023-05-18T14:32:10Z jenkins-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 production 2023-05-18T14:32:10Z jenkins-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 production 2023-05-18T14:32:10Z jenkins-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 production 2023-05-18T14:32:10Z jenkins-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 production 2023-05-18T14:32:10Z jenkins-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 production 2023-05-18T14:32:10Z jenkins-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 production 2023-05-18T14:32:10Z jenkins-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 production 2023-05-18T14:32:10Z jenkins-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 production 2023-05-18T14:32:10Z jenkins-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 production 2023-05-18T14:32:10Z jenkins-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 production 2023-05-18T14:32:10Z jenkins-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 production 2023-05-18T14:32:10Z jenkins-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 production 2023-05-18T14:32:10Z jenkins-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 production 2023-05-18T14:32:10Z jenkins-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", "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", 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 production 2023-05-18T14:32:10Z jenkins-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 production 2023-05-18T14:32:10Z jenkins-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 production 2023-05-18T14:32:10Z jenkins-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 production 2023-05-18T14:32:10Z jenkins-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 production 2023-05-18T14:32:10Z jenkins-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 production 2023-05-18T14:32:10Z jenkins-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 production 2023-05-18T14:32:10Z jenkins-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: ` storagetest123 qYaZm3m8+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 production 2023-05-18T14:32:10Z jenkins-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 production 2023-05-18T14:32:10Z jenkins-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 production 2023-05-18T14:32:10Z jenkins-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 production 2023-05-18T14:32:10Z jenkins-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 production 2023-05-18T14:32:10Z jenkins-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 production 2023-05-18T14:32:10Z jenkins-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 production 2023-05-18T14:32:10Z jenkins-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 production 2023-05-18T14:32:10Z jenkins-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 production 2023-05-18T14:32:10Z jenkins-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 production 2023-05-18T14:32:10Z jenkins-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 production 2023-05-18T14:32:10Z jenkins-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 production 2023-05-18T14:32:10Z jenkins-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 production 2023-05-18T14:32:10Z jenkins-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 production 2023-05-18T14:32:10Z jenkins-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 production 2023-05-18T14:32:10Z jenkins-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 production 2023-05-18T14:32:10Z jenkins-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 production 2023-05-18T14:32:10Z jenkins-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 production 2023-05-18T14:32:10Z jenkins-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 production 2023-05-18T14:32:10Z jenkins-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 production 2023-05-18T14:32:10Z jenkins-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 production 2023-05-18T14:32:10Z jenkins-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 production 2023-05-18T14:32:10Z jenkins-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 production 2023-05-18T14:32:10Z jenkins-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 production 2023-05-18T14:32:10Z jenkins-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 production 2023-05-18T14:32:10Z jenkins-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 production 2023-05-18T14:32:10Z jenkins-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 production 2023-05-18T14:32:10Z jenkins-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 production 2023-05-18T14:32:10Z jenkins-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 production 2023-05-18T14:32:10Z jenkins-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 production 2023-05-18T14:32:10Z jenkins-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 production 2023-05-18T14:32:10Z jenkins-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 production 2023-05-18T14:32:10Z jenkins-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 production 2023-05-18T14:32:10Z jenkins-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 production 2023-05-18T14:32:10Z jenkins-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 production 2023-05-18T14:32:10Z jenkins-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 production 2023-05-18T14:32:10Z jenkins-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 production 2023-05-18T14:32:10Z jenkins-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 production 2023-05-18T14:32:10Z jenkins-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 production 2023-05-18T14:32:10Z jenkins-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 production 2023-05-18T14:32:10Z jenkins-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 production 2023-05-18T14:32:10Z jenkins-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 production 2023-05-18T14:32:10Z jenkins-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 production 2023-05-18T14:32:10Z jenkins-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 production 2023-05-18T14:32:10Z jenkins-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 production 2023-05-18T14:32:10Z jenkins-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%xEKcV&#ePJGn)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%xEKcV&#ePJGn)k0d9a381A26C47380B85F2DB572314-ACBDC267B-8"}, }, { name: "valid pattern - xml", input: ` GLOBAL {bulksms fXHnHK&cN8H!1r5ersTDIe6ZJ8j51} {bulksms AQAAABAAA 04A1ED4D90D3E17-3968-66B6A571D--2134E} configuration for production 2023-05-18T14:32:10Z jenkins-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%xEKcV&#ePJGn)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: ` GLOBAL api-key {buttercms AQAAABAAA xjndr06i2jiaoqs2plf8x0cgfz976blm1dctjqv9} configuration for production 2023-05-18T14:32:10Z jenkins-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.co configuration for production 2023-05-18T14:32:10Z jenkins-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 production 2023-05-18T14:32:10Z jenkins-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