├── .captain └── config.yaml ├── .gitattributes ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── PULL_REQUEST_TEMPLATE.md ├── community_note.md ├── renovate.json └── workflows │ ├── TESTING.md │ ├── codeql-analysis.yml │ ├── detector-tests.yml │ ├── lint.yml │ ├── performance.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 ├── assets └── scanning_logos.svg ├── docs ├── concurrency.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.png │ └── 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 │ │ ├── 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 │ │ ├── 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 │ │ │ └── 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 │ ├── depaware.go │ ├── export_error.go │ ├── filter.go │ ├── filter_test.go │ ├── glob │ │ ├── glob.go │ │ └── glob_test.go │ ├── http.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_test.go │ ├── utf16.go │ ├── utf16_test.dll │ ├── 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 │ ├── 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_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 │ ├── airtableapikey │ │ ├── airtableapikey.go │ │ ├── airtableapikey_integration_test.go │ │ └── airtableapikey_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 │ ├── 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 │ ├── 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 │ │ ├── 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 │ ├── 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 │ │ ├── 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 │ │ ├── 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 │ ├── 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 │ ├── coinbase_waas │ │ ├── coinbase_waas.go │ │ ├── coinbase_waas_integration_test.go │ │ └── coinbase_waas_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 │ ├── 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 │ ├── 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 │ ├── dotmailer │ │ ├── dotmailer.go │ │ ├── dotmailer_integration_test.go │ │ └── dotmailer_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 │ ├── 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 │ ├── 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 │ ├── 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 │ ├── 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 │ │ ├── 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 │ │ ├── mysql.go │ │ ├── mysql_integration_test.go │ │ ├── postgres.go │ │ ├── postgres_integration_test.go │ │ ├── sqlserver.go │ │ └── sqlserver_integration_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 │ ├── 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 │ ├── 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 │ ├── 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 │ ├── 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 │ ├── roninapp │ │ ├── roninapp.go │ │ ├── roninapp_integration_test.go │ │ └── roninapp_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 │ ├── 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 │ ├── 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 │ ├── 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 │ ├── metrics.go │ ├── postman.go │ ├── postman_test.go │ ├── s3.go │ ├── stdin.go │ ├── syslog.go │ ├── testdata │ │ ├── secrets.txt │ │ ├── verificationoverlap_detectors.yaml │ │ ├── verificationoverlap_detectors_fp.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 │ │ ├── dir-archive.zip │ │ ├── example.zip.gz │ │ ├── nested-compressed-archive.tar.gz │ │ ├── nested-dirs.zip │ │ ├── nonarchive.txt │ │ ├── test.deb │ │ ├── test.rpm │ │ ├── test.tar │ │ ├── test.tgz │ │ └── testdir.zip ├── hasher │ ├── blake2b.go │ ├── hasher.go │ └── hasher_test.go ├── iobuf │ ├── bufferedreaderseeker.go │ └── bufferedreaderseeker_test.go ├── log │ ├── dynamic_redactor.go │ ├── level.go │ ├── log.go │ ├── log_test.go │ └── redaction_core.go ├── output │ ├── github_actions.go │ ├── json.go │ ├── legacy_json.go │ └── plain.go ├── pb │ ├── 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 │ │ ├── docker.go │ │ ├── docker_test.go │ │ └── metrics.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_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 │ │ ├── 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 │ │ ├── metrics.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 │ ├── huggingface │ │ ├── client.go │ │ ├── huggingface.go │ │ ├── huggingface_client_test.go │ │ ├── huggingface_test.go │ │ └── repo.go │ ├── jenkins │ │ ├── jenkins.go │ │ └── jenkins_integration_test.go │ ├── job_progress.go │ ├── job_progress_hook.go │ ├── job_progress_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 │ ├── source_manager.go │ ├── source_manager_test.go │ ├── source_unit.go │ ├── sources.go │ ├── sources_test.go │ ├── stdin │ │ └── stdin.go │ ├── syslog │ │ └── syslog.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 ├── 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 /.captain/config.yaml: -------------------------------------------------------------------------------- 1 | test-suites: 2 | detectors: 3 | command: gotestsum --jsonfile tmp/go-test.json --raw-command -- go test -tags=detectors -timeout=15m -json -count=1 -vet=off ./pkg/detectors/... 4 | results: 5 | path: tmp/go-test.json 6 | output: 7 | print-summary: true 8 | ## No retries right now 9 | # retries: 10 | # attempts: 3 11 | # command: gotestsum --raw-command --jsonfile tmp/go-test.json -- go test -tags=detectors -timeout=15m -json -count=1 -vet=off {{ package }} -run '{{ run }}' 12 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.go text eol=lf 2 | *.md text eol=lf -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "" 5 | labels: bug, needs triage 6 | assignees: trufflesecurity/product-eng 7 | --- 8 | 9 | Please review the [Community Note](https://github.com/trufflesecurity/trufflehog/blob/main/.github/community_note.md) before submitting 10 | 11 | ### TruffleHog Version 12 | 13 | 14 | ### Trace Output 15 | 16 | 21 | 22 | ### Expected Behavior 23 | 24 | 25 | 26 | ### Actual Behavior 27 | 28 | 29 | 30 | ### Steps to Reproduce 31 | 32 | 33 | 1. Go to '...' 34 | 2. Click on '....' 35 | 3. Scroll down to '....' 36 | 4. See error 37 | 38 | ## Environment 39 | * OS: [e.g. iOS] 40 | * Version [e.g. 22] 41 | 42 | ## Additional Context 43 | 44 | 45 | ### References 46 | 47 | 52 | 53 | * #0000 54 | 55 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: "" 5 | labels: enhancement, needs triage 6 | assignees: trufflesecurity/product-eng 7 | --- 8 | 9 | Please review the [Community Note](https://github.com/trufflesecurity/trufflehog/blob/main/.github/community_note.md) before submitting 10 | 11 | ## Description 12 | 13 | 14 | ### Preferred Solution 15 | 18 | 19 | ### Additional Context 20 | 21 | 22 | #### References 23 | 24 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 5 | 6 | ### Description: 7 | Explain the purpose of the PR. 8 | 9 | ### Checklist: 10 | * [ ] Tests passing (`make test-community`)? 11 | * [ ] Lint passing (`make lint` this requires [golangci-lint](https://golangci-lint.run/welcome/install/#local-installation))? 12 | -------------------------------------------------------------------------------- /.github/community_note.md: -------------------------------------------------------------------------------- 1 | ## Community Note 2 | 3 | Please vote on this issue by adding a 👍 reaction to the original issue to help the community and maintainers prioritize this request. 4 | 5 | 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. 6 | 7 | If you are interested in working on this issue or have submitted a pull request, please leave a comment. 8 | -------------------------------------------------------------------------------- /.github/renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "config:base" 5 | ], 6 | "prConcurrentLimit": 3, 7 | "prHourlyLimit": 2 8 | } 9 | -------------------------------------------------------------------------------- /.github/workflows/TESTING.md: -------------------------------------------------------------------------------- 1 | # Testing 2 | 3 | Most testing is handled automatically by our GitHub Actions workflows. 4 | 5 | ## Local GitHub Action Testing 6 | 7 | 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. 8 | 9 | Fortunately [nektos/act](https://github.com/nektos/act) enables local testing of GitHub Actions. 10 | 11 | ### Instructions 12 | 13 | 1. Please follow [the installation instructions](http://https://github.com/nektos/act#installation) for your OS. 14 | 2. The first run of `act` will ask you to specify an image. `Medium` should suffice. 15 | 3. You'll need to configure a personal-access-token(PAT) with: `repo:status`, `repo_deployment`, and `public_repo` permissions. 16 | 4. Set an environment variable named `GITHUB_TOKEN` with the PAT from the previous step as the value: `$ export GITHUB_TOKEN=` 17 | 5. Run the following command from the repository root: `act pull_request -j test -W .github/workflows/secrets.yml -s GITHUB_TOKEN --defaultbranch main` 18 | 6. If the job was successful, you should expect to see output from the scanner showing several detected secrets. 19 | 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` 20 | -------------------------------------------------------------------------------- /.github/workflows/detector-tests.yml: -------------------------------------------------------------------------------- 1 | name: Detectors Aggregation 2 | 3 | on: 4 | workflow_dispatch: 5 | schedule: 6 | - cron: "0 8 * * *" 7 | 8 | jobs: 9 | test-detectors: 10 | if: ${{ github.repository == 'trufflesecurity/trufflehog' }} 11 | runs-on: ubuntu-latest 12 | permissions: 13 | actions: "read" 14 | contents: "read" 15 | id-token: "write" 16 | steps: 17 | - uses: actions/checkout@v4 18 | - uses: actions/setup-go@v5 19 | - name: Install gotestsum 20 | uses: jaxxstorm/action-install-gh-release@v1.14.0 21 | with: 22 | repo: gotestyourself/gotestsum 23 | - uses: rwx-research/setup-captain@v1 24 | - name: Test Go 25 | run: | 26 | export CGO_ENABLED=1 27 | captain run detectors 28 | env: 29 | RWX_ACCESS_TOKEN: ${{ secrets.RWX_ACCESS_TOKEN }} 30 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Lint 2 | 3 | on: 4 | push: 5 | tags: 6 | - v* 7 | pull_request: 8 | 9 | permissions: 10 | contents: read 11 | pull-requests: read 12 | 13 | jobs: 14 | golangci-lint: 15 | name: golangci-lint 16 | runs-on: ubuntu-latest 17 | steps: 18 | - uses: actions/checkout@v4 19 | - uses: actions/setup-go@v5 20 | with: 21 | go-version: "1.24" 22 | - name: golangci-lint 23 | uses: golangci/golangci-lint-action@v6 24 | with: 25 | # Optional: version of golangci-lint to use in form of v1.2 or v1.2.3 or `latest` to use the latest version 26 | version: latest 27 | # Optional: working directory, useful for monorepos 28 | # working-directory: somedir 29 | 30 | # Optional: golangci-lint command line arguments. 31 | args: --enable bodyclose --enable copyloopvar --enable misspell --timeout 10m 32 | 33 | # Optional: if set to true then the action don't cache or restore ~/go/pkg. 34 | # skip-pkg-cache: true 35 | 36 | # Optional: if set to true then the action don't cache or restore ~/.cache/go-build. 37 | # skip-build-cache: true 38 | semgrep: 39 | name: semgrep 40 | runs-on: ubuntu-latest 41 | container: 42 | image: returntocorp/semgrep 43 | if: (github.actor != 'dependabot[bot]') 44 | steps: 45 | - uses: actions/checkout@v4 46 | - run: semgrep --config=hack/semgrep-rules/detectors.yaml pkg/detectors/ 47 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - v* 7 | 8 | permissions: 9 | contents: write 10 | packages: write 11 | id-token: write 12 | 13 | jobs: 14 | Release: 15 | runs-on: ubuntu-latest 16 | env: 17 | DOCKER_CLI_EXPERIMENTAL: "enabled" 18 | steps: 19 | - name: Checkout 20 | uses: actions/checkout@v4 21 | with: 22 | fetch-depth: 0 23 | - name: Set up QEMU 24 | uses: docker/setup-qemu-action@v3 25 | - name: Docker Login to DockerHub 26 | uses: docker/login-action@v3 27 | with: 28 | username: ${{ secrets.DOCKERHUB_USERNAME }} 29 | password: ${{ secrets.DOCKERHUB_TOKEN }} 30 | - name: Docker Login to GitHub Container Registry 31 | uses: docker/login-action@v3 32 | with: 33 | registry: ghcr.io 34 | username: ${{ github.repository_owner }} 35 | password: ${{ secrets.GITHUB_TOKEN }} 36 | - name: Set up Go 37 | uses: actions/setup-go@v5 38 | with: 39 | go-version: "1.24" 40 | - name: Cosign install 41 | uses: sigstore/cosign-installer@d7d6bc7722e3daa8354c50bcb52f4837da5e9b6a # v3.8.1 42 | - name: Install UPX 43 | run: | 44 | sudo apt-get update 45 | sudo apt-get install -y upx 46 | - name: Run GoReleaser 47 | uses: goreleaser/goreleaser-action@v6 48 | with: 49 | distribution: goreleaser-pro 50 | version: latest 51 | args: release --clean 52 | env: 53 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 54 | HOMEBREW_TAP_TOKEN: ${{ secrets.HOMEBREW_TAP_TOKEN }} 55 | GORELEASER_KEY: ${{ secrets.GORELEASER_KEY }} 56 | -------------------------------------------------------------------------------- /.github/workflows/secrets.yml: -------------------------------------------------------------------------------- 1 | name: Scan for secrets 2 | 3 | on: 4 | push: 5 | tags: 6 | - v* 7 | branches: 8 | - main 9 | pull_request: 10 | workflow_dispatch: 11 | 12 | jobs: 13 | test: 14 | if: ${{ github.repository == 'trufflesecurity/trufflehog' && !github.event.pull_request.head.repo.fork }} 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: Checkout code 18 | uses: actions/checkout@v4 19 | with: 20 | fetch-depth: 0 21 | ref: ${{ github.head_ref }} 22 | - name: Dogfood 23 | uses: ./ 24 | id: dogfood 25 | with: 26 | extra_args: --results=verified 27 | -------------------------------------------------------------------------------- /.github/workflows/smoke.yml: -------------------------------------------------------------------------------- 1 | name: Smoke 2 | 3 | on: 4 | pull_request: 5 | 6 | jobs: 7 | smoke: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Checkout code 11 | uses: actions/checkout@v4 12 | - name: Install Go 13 | uses: actions/setup-go@v5 14 | with: 15 | go-version: "1.24" 16 | - name: Smoke 17 | run: | 18 | set -e 19 | go run . git https://github.com/dustin-decker/secretsandstuff.git > /dev/null 20 | go run . github --repo https://github.com/dustin-decker/secretsandstuff.git > /dev/null 21 | zombies: 22 | runs-on: ubuntu-latest 23 | timeout-minutes: 5 24 | steps: 25 | - name: Checkout code 26 | uses: actions/checkout@v4 27 | - name: Install Go 28 | uses: actions/setup-go@v5 29 | with: 30 | go-version: "1.24" 31 | - name: Run trufflehog 32 | run: | 33 | set -e 34 | go run . git --no-verification file://. > /dev/null 35 | # This case previously had a deadlock issue and left zombies after trufflehog exited #3379 36 | go run . git --no-verification https://github.com/git-test-fixtures/binary.git > /dev/null 37 | - name: Check for running git processes and zombies 38 | run: | 39 | if pgrep -x "git" > /dev/null 40 | then 41 | echo "Git processes are still running" 42 | exit 1 43 | else 44 | echo "No git processes found" 45 | fi 46 | 47 | if ps -A -ostat,ppid | grep -e '[zZ]' > /dev/null 48 | then 49 | echo "Zombie processes found" 50 | exit 1 51 | else 52 | echo "No zombie processes found" 53 | fi 54 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | dist 3 | .env 4 | *.test 5 | 6 | # binary 7 | trufflehog 8 | tmp/go-test.json 9 | .captain/detectors/timings.yaml 10 | .captain/detectors/quarantines.yaml 11 | .captain/detectors/flakes.yaml 12 | .vscode 13 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/rhysd/actionlint 3 | rev: v1.6.24 4 | hooks: 5 | - id: actionlint 6 | 7 | - repo: https://github.com/mpalmer/action-validator 8 | rev: v0.5.1 9 | hooks: 10 | - id: action-validator 11 | -------------------------------------------------------------------------------- /.pre-commit-hooks.yaml: -------------------------------------------------------------------------------- 1 | - id: trufflehog 2 | name: TruffleHog 3 | description: Detect secrets in your data with TruffleHog. 4 | entry: trufflehog git file://. --since-commit HEAD --results=verified --fail 5 | language: golang 6 | pass_filenames: false 7 | -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | # catch-all 2 | * @trufflesecurity/product-eng 3 | 4 | # Scanning 5 | pkg/writers/ @trufflesecurity/Scanning 6 | 7 | # Shared 8 | pkg/decoders/ @trufflesecurity/Scanning @trufflesecurity/OSS 9 | pkg/engine/ @trufflesecurity/Scanning @trufflesecurity/OSS 10 | pkg/gitparse/ @trufflesecurity/Scanning @trufflesecurity/OSS 11 | pkg/giturl/ @trufflesecurity/Scanning @trufflesecurity/OSS 12 | pkg/handlers/ @trufflesecurity/Scanning @trufflesecurity/OSS 13 | pkg/iobuf/ @trufflesecurity/Scanning @trufflesecurity/OSS 14 | pkg/sanitizer/ @trufflesecurity/Scanning @trufflesecurity/OSS 15 | pkg/sources/ @trufflesecurity/Scanning @trufflesecurity/OSS 16 | proto/ @trufflesecurity/Scanning @trufflesecurity/OSS 17 | 18 | # OSS 19 | pkg/detectors/ @trufflesecurity/OSS 20 | pkg/common/ @trufflesecurity/OSS 21 | pkg/custom_detectors/ @trufflesecurity/OSS 22 | pkg/analzyers/ @trufflesecurity/OSS 23 | 24 | # critical detectors 25 | pkg/detectors/aws/ @trufflesecurity/backend 26 | pkg/detectors/gcp/ @trufflesecurity/backend 27 | pkg/detectors/azure/ @trufflesecurity/backend 28 | pkg/detectors/okta/ @trufflesecurity/backend 29 | pkg/detectors/privatekey/ @trufflesecurity/backend 30 | pkg/detectors/slack/ @trufflesecurity/backend 31 | pkg/detectors/slackwebhook/ @trufflesecurity/backend 32 | pkg/detectors/microsoftteamswebhook/ @trufflesecurity/backend 33 | pkg/detectors/twilio/ @trufflesecurity/backend 34 | pkg/detectors/sendgrid/ @trufflesecurity/backend 35 | pkg/detectors/gitlab/ @trufflesecurity/backend 36 | pkg/detectors/gitlabv2/ @trufflesecurity/backend 37 | pkg/detectors/github/ @trufflesecurity/backend 38 | pkg/detectors/github_old/ @trufflesecurity/backend 39 | pkg/detectors/githubapp/ @trufflesecurity/backend 40 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM --platform=${BUILDPLATFORM} golang:bullseye as builder 2 | 3 | WORKDIR /build 4 | COPY . . 5 | ENV CGO_ENABLED=0 6 | ARG TARGETOS TARGETARCH 7 | RUN --mount=type=cache,target=/go/pkg/mod \ 8 | --mount=type=cache,target=/root/.cache/go-build \ 9 | GOOS=${TARGETOS} GOARCH=${TARGETARCH} go build -o trufflehog . 10 | 11 | FROM alpine:3.21 12 | RUN apk add --no-cache bash git openssh-client ca-certificates rpm2cpio binutils cpio \ 13 | && rm -rf /var/cache/apk/* && update-ca-certificates 14 | COPY --from=builder /build/trufflehog /usr/bin/trufflehog 15 | COPY entrypoint.sh /etc/entrypoint.sh 16 | RUN chmod +x /etc/entrypoint.sh 17 | ENTRYPOINT ["/etc/entrypoint.sh"] 18 | -------------------------------------------------------------------------------- /Dockerfile.goreleaser: -------------------------------------------------------------------------------- 1 | FROM alpine:3.21 2 | 3 | RUN apk add --no-cache bash git openssh-client ca-certificates \ 4 | && rm -rf /var/cache/apk/* && update-ca-certificates 5 | WORKDIR /usr/bin/ 6 | COPY trufflehog . 7 | COPY entrypoint.sh /etc/entrypoint.sh 8 | RUN chmod +x /etc/entrypoint.sh 9 | ENTRYPOINT ["/etc/entrypoint.sh"] 10 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | Please report any security issues to security@trufflesec.com and include `trufflehog` in the subject -------------------------------------------------------------------------------- /entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Parse the last argument into an array of extra_args. 4 | mapfile -t extra_args < <(bash -c "for arg in ${*: -1}; do echo \$arg; done") 5 | 6 | # Directories might be owned by a user other than root 7 | git config --global --add safe.directory '*' 8 | 9 | if [[ $# -eq 0 ]]; then 10 | /usr/bin/trufflehog --help 11 | else 12 | /usr/bin/trufflehog "${@: 1: $#-1}" "${extra_args[@]}" 13 | fi 14 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | # Examples 2 | This folder contains various examples like custom detectors, scripts, etc. Feel free to contribute! 3 | 4 | ### Generic Detector 5 | 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. 6 | 7 | #### Try it out: 8 | ``` 9 | wget https://raw.githubusercontent.com/trufflesecurity/trufflehog/main/examples/generic.yml 10 | trufflehog filesystem --config=$PWD/generic.yml $PWD 11 | 12 | # to filter so that _only_ generic credentials are logged: 13 | trufflehog filesystem --config=$PWD/generic.yml --json --no-verification $PWD | awk '/generic-api-key/{print $0}' 14 | ``` 15 | -------------------------------------------------------------------------------- /examples/generic.yml: -------------------------------------------------------------------------------- 1 | detectors: 2 | - name: generic-api-key 3 | keywords: 4 | - key 5 | - api 6 | - token 7 | - secret 8 | - client 9 | - passwd 10 | - password 11 | - auth 12 | - access 13 | regex: 14 | # borrowing the gitleaks generic-api-key regex 15 | 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|;]|$)" 16 | -------------------------------------------------------------------------------- /hack/bench/plot.gp: -------------------------------------------------------------------------------- 1 | set terminal png size 800,600 2 | set output "hack/bench/versions.png" 3 | 4 | set title "User Time vs. Version" 5 | set xlabel "Version" 6 | set ylabel "Average User Time (s)" 7 | 8 | set xtics rotate by -45 9 | 10 | plot "hack/bench/plot.txt" using 2:xtic(1) with linespoints linestyle 1 notitle 11 | -------------------------------------------------------------------------------- /hack/bench/plot.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if [ $# -ne 2 ]; then 4 | echo "Usage: $0 " 5 | exit 1 6 | fi 7 | 8 | # Get the number of versions back to test from command line argument 9 | num_versions="$2" 10 | 11 | test_repo="$1" 12 | 13 | bash hack/bench/versions.sh $test_repo $num_versions | tee hack/bench/plot.txt 14 | 15 | gnuplot hack/bench/plot.gp 16 | -------------------------------------------------------------------------------- /hack/bench/plot.txt: -------------------------------------------------------------------------------- 1 | v3.33.0: 1.402 2 | v3.32.2: 1.298 3 | v3.32.1: 1.332 4 | v3.32.0: 1.348 5 | v3.31.6: 2.470 6 | v3.31.5: 2.462 7 | v3.31.4: 2.460 8 | v3.31.3: 2.418 9 | v3.31.2: 1.384 10 | v3.31.1: 1.344 11 | v3.31.0: 1.354 12 | v3.30.0: 1.392 13 | v3.29.1: 1.382 14 | v3.29.0: 1.340 15 | v3.28.7: 1.380 16 | v3.28.6: 1.308 17 | v3.28.5: 2.596 18 | v3.28.4: 2.554 19 | v3.28.3: 2.582 20 | v3.28.1: 2.578 21 | v3.28.2: 2.566 22 | v3.28.0: 2.552 23 | v3.27.1: 2.574 24 | v3.26.0: 2.538 25 | -------------------------------------------------------------------------------- /hack/bench/versions.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trufflesecurity/trufflehog/5fca1636cda0ebc323168d8d46746ce02786280c/hack/bench/versions.png -------------------------------------------------------------------------------- /hack/generate/test.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -eu 3 | 4 | function cleanup { 5 | rm -rf pkg/detectors/test 6 | } 7 | trap cleanup EXIT 8 | 9 | export CGO_ENABLED=0 10 | 11 | export FORCE_PASS_DIFF=true 12 | 13 | echo "████████████ Testing generate Detector" 14 | go run hack/generate/generate.go detector Test 15 | go test ./pkg/detectors/test -benchmem -bench . 16 | echo "" 17 | -------------------------------------------------------------------------------- /hack/semgrep-rules/detectors.yaml: -------------------------------------------------------------------------------- 1 | rules: 2 | - id: no-printing-in-detectors 3 | patterns: 4 | - pattern-either: 5 | - pattern: fmt.Println(...) 6 | - pattern: fmt.Printf(...) 7 | - pattern: import("log") 8 | message: "Do not print or log inside of detectors." 9 | languages: [go] 10 | severity: ERROR 11 | -------------------------------------------------------------------------------- /hack/snifftest/README.md: -------------------------------------------------------------------------------- 1 | # snifftest 2 | 3 | See the help pages with this command, or look further below to get started quickly. 4 | 5 | ``` 6 | go run hack/snifftest/main.go 7 | ``` 8 | 9 | ## Show available secret scanners 10 | 11 | ``` 12 | go run hack/snifftest/main.go show-scanners 13 | ``` 14 | 15 | ## Scan 16 | 17 | All scanners 18 | 19 | ``` 20 | go run snifftest/main.go scan --db ~/sdb --scanner all --print 21 | ``` 22 | 23 | Particular scanner 24 | 25 | ``` 26 | go run snifftest/main.go scan --db ~/sdb --scanner github --print --print-chunk --fail-threshold 5 27 | ``` 28 | -------------------------------------------------------------------------------- /hack/snifftest/snifftest.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | REPO_ARRAY=( 4 | "https://github.com/Netflix/Hystrix.git" 5 | # "https://github.com/facebook/flow.git" 6 | # "https://github.com/Netflix/vizceral.git" 7 | # "https://github.com/Netflix/metaflow.git" 8 | # "https://github.com/Netflix/dgs-framework.git" 9 | # "https://github.com/Netflix/vector.git" 10 | # "https://github.com/expressjs/express.git" 11 | # "https://github.com/Azure/azure-sdk-for-net" 12 | # "https://github.com/Azure/azure-cli" 13 | ) 14 | REPOS=$(printf "%s," "${REPO_ARRAY[@]}" | cut -d "," -f 1-${#REPO_ARRAY[@]}) 15 | go run hack/snifftest/main.go scan --exclude privatekey --exclude uri --exclude github_old --repo "$REPOS" --detector all --print --fail-threshold 99 -------------------------------------------------------------------------------- /pkg/analyzer/README.md: -------------------------------------------------------------------------------- 1 | # Implementing Analyzers 2 | 3 | ## Defining the Permissions 4 | 5 | Permissions can be defined in: 6 | - lower snake case as `permission_name:access_level` 7 | - kebab case as `permission-name:read` 8 | - dot notation as `permission.name:read` 9 | 10 | The Permissions are initially defined as a [yaml file](analyzers/twilio/permissions.yaml). 11 | 12 | At the top of the [analyzer implementation](analyzers/twilio/twilio.go) you specify the go generate command. 13 | 14 | You can install the generator with `go install github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/generate_permissions`. 15 | 16 | Then you can run `go generate ./...` to generate the Permission types for the analyzer. 17 | 18 | The generated Permission types are to be used in the `AnalyzerResult` struct when defining the `Permissions` and in your code. 19 | -------------------------------------------------------------------------------- /pkg/analyzer/analyzers/airbrake/scopes.go: -------------------------------------------------------------------------------- 1 | package airbrake 2 | 3 | var scope_order = []string{ 4 | "Authentication", 5 | "Performance Monitoring", 6 | "Error Notification", 7 | "Projects", 8 | "Deploys", 9 | "Groups", 10 | "Notices", 11 | "Project Activities", 12 | "Source Maps", 13 | "iOS Crash Reports", 14 | } 15 | 16 | var scope_mapping = map[string][]string{ 17 | "Authentication": {"Create user token"}, 18 | "Performance Monitoring": {"Route performance endpoint", "Routes breakdown endpoint", "Database query stats", "Queue stats"}, 19 | "Error Notification": {"Create notice"}, 20 | "Projects": {"List projects", "Show projects"}, 21 | "Deploys": {"Create deploy", "List deploys", "Show deploy"}, 22 | "Groups": {"List groups", "Show group", "Mute group", "Unmute group", "Delete group", "List groups across all projects", "Show group statistics"}, 23 | "Notices": {"List notices", "Show notice status"}, 24 | "Project Activities": {"List project activities", "Show project statistics"}, 25 | "Source Maps": {"Create source map", "List source maps", "Show source map", "Delete source map"}, 26 | "iOS Crash Reports": {"Create iOS crash report"}, 27 | } 28 | -------------------------------------------------------------------------------- /pkg/analyzer/analyzers/airtable/airtableoauth/expected_output.json: -------------------------------------------------------------------------------- 1 | { 2 | "AnalyzerType": 28, 3 | "Bindings": [ 4 | { 5 | "Resource": { 6 | "Name": "usraS0CjAASH3XMpU", 7 | "FullyQualifiedName": "usraS0CjAASH3XMpU", 8 | "Type": "user", 9 | "Metadata": {}, 10 | "Parent": null 11 | }, 12 | "Permission": { 13 | "Value": "data.records:read", 14 | "Parent": null 15 | } 16 | }, 17 | { 18 | "Resource": { 19 | "Name": "usraS0CjAASH3XMpU", 20 | "FullyQualifiedName": "usraS0CjAASH3XMpU", 21 | "Type": "user", 22 | "Metadata": {}, 23 | "Parent": null 24 | }, 25 | "Permission": { 26 | "Value": "schema.bases:read", 27 | "Parent": null 28 | } 29 | } 30 | ], 31 | "UnboundedResources": [ 32 | { 33 | "Name": "Client Leads and Sales Management", 34 | "FullyQualifiedName": "appzRyj5Q9R9kK6cF", 35 | "Type": "base", 36 | "Parent": null 37 | } 38 | ] 39 | } -------------------------------------------------------------------------------- /pkg/analyzer/analyzers/airtable/airtablepat/expected_output.json: -------------------------------------------------------------------------------- 1 | { 2 | "AnalyzerType": 29, 3 | "Bindings": [ 4 | { 5 | "Resource": { 6 | "Name": "usraS0CjAASH3XMpU", 7 | "FullyQualifiedName": "usraS0CjAASH3XMpU", 8 | "Type": "user", 9 | "Metadata": {}, 10 | "Parent": null 11 | }, 12 | "Permission": { 13 | "Value": "data.records:read", 14 | "Parent": null 15 | } 16 | }, 17 | { 18 | "Resource": { 19 | "Name": "usraS0CjAASH3XMpU", 20 | "FullyQualifiedName": "usraS0CjAASH3XMpU", 21 | "Type": "user", 22 | "Metadata": {}, 23 | "Parent": null 24 | }, 25 | "Permission": { 26 | "Value": "schema.bases:read", 27 | "Parent": null 28 | } 29 | } 30 | ], 31 | "UnboundedResources": [ 32 | { 33 | "Name": "Client Leads and Sales Management", 34 | "FullyQualifiedName": "appzRyj5Q9R9kK6cF", 35 | "Type": "base", 36 | "Parent": null 37 | } 38 | ] 39 | } -------------------------------------------------------------------------------- /pkg/analyzer/analyzers/airtable/airtablepat/requests.go: -------------------------------------------------------------------------------- 1 | package airtablepat 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "net/http" 7 | "strings" 8 | 9 | "github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers/airtable/common" 10 | ) 11 | 12 | type AirtableRecordsResponse struct { 13 | Records []common.AirtableEntity `json:"records"` 14 | } 15 | 16 | func fetchAirtableRecords(token string, baseID string, tableID string) ([]common.AirtableEntity, error) { 17 | endpoint, exists := getEndpoint(common.ListRecordsEndpoint) 18 | if !exists { 19 | return nil, fmt.Errorf("endpoint for ListRecordsEndpoint does not exist") 20 | } 21 | url := strings.Replace(strings.Replace(endpoint.URL, "{baseID}", baseID, -1), "{tableID}", tableID, -1) 22 | resp, err := common.CallAirtableAPI(token, "GET", url) 23 | if err != nil { 24 | return nil, err 25 | } 26 | defer resp.Body.Close() 27 | 28 | if resp.StatusCode != http.StatusOK { 29 | return nil, fmt.Errorf("failed to fetch Airtable records, status: %d", resp.StatusCode) 30 | } 31 | 32 | var recordsResponse AirtableRecordsResponse 33 | if err := json.NewDecoder(resp.Body).Decode(&recordsResponse); err != nil { 34 | return nil, err 35 | } 36 | 37 | return recordsResponse.Records, nil 38 | } 39 | -------------------------------------------------------------------------------- /pkg/analyzer/analyzers/airtable/common/models.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | type AirtableUserInfo struct { 4 | ID string `json:"id"` 5 | Email *string `json:"email,omitempty"` 6 | Scopes []string `json:"scopes"` 7 | } 8 | 9 | type AirtableBases struct { 10 | Bases []struct { 11 | ID string `json:"id"` 12 | Name string `json:"name"` 13 | Schema *Schema `json:"schema,omitempty"` 14 | } `json:"bases"` 15 | } 16 | 17 | type Schema struct { 18 | Tables []AirtableEntity `json:"tables"` 19 | } 20 | 21 | type AirtableEntity struct { 22 | ID string `json:"id"` 23 | } 24 | -------------------------------------------------------------------------------- /pkg/analyzer/analyzers/airtable/common/permissions.yaml: -------------------------------------------------------------------------------- 1 | permissions: 2 | - data.records:read 3 | - data.records:write 4 | - data.recordComments:read 5 | - data.recordComments:write 6 | - schema.bases:read 7 | - schema.bases:write 8 | - webhook:manage 9 | - block:manage 10 | - user.email:read 11 | - enterprise.groups:read 12 | - workspacesAndBases:read 13 | - workspacesAndBases:write 14 | - workspacesAndBases.shares:manage 15 | - enterprise.scim.usersAndGroups:manage 16 | - enterprise.auditLogs:read 17 | - enterprise.changeEvents:read 18 | - enterprise.exports:manage 19 | - enterprise.account:read 20 | - enterprise.account:write 21 | - enterprise.user:read 22 | - enterprise.user:write 23 | - enterprise.groups:manage 24 | - workspacesAndBases:manage 25 | -------------------------------------------------------------------------------- /pkg/analyzer/analyzers/anthropic/permissions.go: -------------------------------------------------------------------------------- 1 | // Code generated by go generate; DO NOT EDIT. 2 | package anthropic 3 | 4 | import "errors" 5 | 6 | type Permission int 7 | 8 | const ( 9 | Invalid Permission = iota 10 | FullAccess Permission = iota 11 | ) 12 | 13 | var ( 14 | PermissionStrings = map[Permission]string{ 15 | FullAccess: "full_access", 16 | } 17 | 18 | StringToPermission = map[string]Permission{ 19 | "full_access": FullAccess, 20 | } 21 | 22 | PermissionIDs = map[Permission]int{ 23 | FullAccess: 1, 24 | } 25 | 26 | IdToPermission = map[int]Permission{ 27 | 1: FullAccess, 28 | } 29 | ) 30 | 31 | // ToString converts a Permission enum to its string representation 32 | func (p Permission) ToString() (string, error) { 33 | if str, ok := PermissionStrings[p]; ok { 34 | return str, nil 35 | } 36 | return "", errors.New("invalid permission") 37 | } 38 | 39 | // ToID converts a Permission enum to its ID 40 | func (p Permission) ToID() (int, error) { 41 | if id, ok := PermissionIDs[p]; ok { 42 | return id, nil 43 | } 44 | return 0, errors.New("invalid permission") 45 | } 46 | 47 | // PermissionFromString converts a string representation to its Permission enum 48 | func PermissionFromString(s string) (Permission, error) { 49 | if p, ok := StringToPermission[s]; ok { 50 | return p, nil 51 | } 52 | return 0, errors.New("invalid permission string") 53 | } 54 | 55 | // PermissionFromID converts an ID to its Permission enum 56 | func PermissionFromID(id int) (Permission, error) { 57 | if p, ok := IdToPermission[id]; ok { 58 | return p, nil 59 | } 60 | return 0, errors.New("invalid permission ID") 61 | } 62 | -------------------------------------------------------------------------------- /pkg/analyzer/analyzers/anthropic/permissions.yaml: -------------------------------------------------------------------------------- 1 | permissions: 2 | - full_access 3 | -------------------------------------------------------------------------------- /pkg/analyzer/analyzers/asana/permissions.yaml: -------------------------------------------------------------------------------- 1 | permissions: 2 | - allocations:read 3 | - allocations:write 4 | - attachments:read 5 | - attachments:write 6 | - autdit_logs:read 7 | - autdit_logs:write 8 | - custom_fields:read 9 | - custom_fields:write 10 | - custom_field_settings:read 11 | - custom_field_settings:write 12 | - batch_api:read 13 | - batch_api:write 14 | - events:read 15 | - events:write 16 | - goals:read 17 | - goals:write 18 | - jobs:read 19 | - jobs:write 20 | - portfolios:read 21 | - portfolios:write 22 | - projects:read 23 | - projects:write 24 | - project_memberships:read 25 | - project_memberships:write 26 | - sections:read 27 | - sections:write 28 | - tags:read 29 | - tags:write 30 | - tasks:read 31 | - tasks:write 32 | - teams:read 33 | - teams:write 34 | - users:read 35 | - users:write 36 | - user_task_lists:read 37 | - user_task_lists:write 38 | - memberships:read 39 | - memberships:write 40 | - rules:read 41 | - rules:write 42 | -------------------------------------------------------------------------------- /pkg/analyzer/analyzers/bitbucket/permissions.yaml: -------------------------------------------------------------------------------- 1 | permissions: 2 | - project 3 | - project:admin 4 | - repository 5 | - repository:write 6 | - repository:admin 7 | - repository:delete 8 | - pullrequest 9 | - pullrequest:write 10 | - webhook 11 | - account 12 | - pipeline 13 | - pipeline:write 14 | - pipeline:variable 15 | - runner 16 | - runner:write 17 | -------------------------------------------------------------------------------- /pkg/analyzer/analyzers/databricks/permissions.go: -------------------------------------------------------------------------------- 1 | // Code generated by go generate; DO NOT EDIT. 2 | package databricks 3 | 4 | import "errors" 5 | 6 | type Permission int 7 | 8 | const ( 9 | Invalid Permission = iota 10 | CanManage Permission = iota 11 | CanUse Permission = iota 12 | ) 13 | 14 | var ( 15 | PermissionStrings = map[Permission]string{ 16 | CanManage: "CAN_MANAGE", 17 | CanUse: "CAN_USE", 18 | } 19 | 20 | StringToPermission = map[string]Permission{ 21 | "CAN_MANAGE": CanManage, 22 | "CAN_USE": CanUse, 23 | } 24 | 25 | PermissionIDs = map[Permission]int{ 26 | CanManage: 1, 27 | CanUse: 2, 28 | } 29 | 30 | IdToPermission = map[int]Permission{ 31 | 1: CanManage, 32 | 2: CanUse, 33 | } 34 | ) 35 | 36 | // ToString converts a Permission enum to its string representation 37 | func (p Permission) ToString() (string, error) { 38 | if str, ok := PermissionStrings[p]; ok { 39 | return str, nil 40 | } 41 | return "", errors.New("invalid permission") 42 | } 43 | 44 | // ToID converts a Permission enum to its ID 45 | func (p Permission) ToID() (int, error) { 46 | if id, ok := PermissionIDs[p]; ok { 47 | return id, nil 48 | } 49 | return 0, errors.New("invalid permission") 50 | } 51 | 52 | // PermissionFromString converts a string representation to its Permission enum 53 | func PermissionFromString(s string) (Permission, error) { 54 | if p, ok := StringToPermission[s]; ok { 55 | return p, nil 56 | } 57 | return 0, errors.New("invalid permission string") 58 | } 59 | 60 | // PermissionFromID converts an ID to its Permission enum 61 | func PermissionFromID(id int) (Permission, error) { 62 | if p, ok := IdToPermission[id]; ok { 63 | return p, nil 64 | } 65 | return 0, errors.New("invalid permission ID") 66 | } 67 | -------------------------------------------------------------------------------- /pkg/analyzer/analyzers/databricks/permissions.yaml: -------------------------------------------------------------------------------- 1 | permissions: 2 | - CAN_MANAGE 3 | - CAN_USE 4 | -------------------------------------------------------------------------------- /pkg/analyzer/analyzers/dockerhub/permissions.yaml: -------------------------------------------------------------------------------- 1 | permissions: 2 | - repo:read 3 | - repo:write 4 | - repo:admin 5 | - repo:public_read 6 | -------------------------------------------------------------------------------- /pkg/analyzer/analyzers/dockerhub/result_output.json: -------------------------------------------------------------------------------- 1 | { 2 | "AnalyzerType": 4, 3 | "Bindings": [ 4 | { 5 | "Resource": { 6 | "Name": "test-private", 7 | "FullyQualifiedName": "truffledockerman/repo/image/test-private", 8 | "Type": "image", 9 | "Metadata": { 10 | "is_private": true, 11 | "pull_count": 0, 12 | "star_count": 0 13 | }, 14 | "Parent": null 15 | }, 16 | "Permission": { 17 | "Value": "repo:admin", 18 | "Parent": null 19 | } 20 | }, 21 | { 22 | "Resource": { 23 | "Name": "test", 24 | "FullyQualifiedName": "truffledockerman/repo/image/test", 25 | "Type": "image", 26 | "Metadata": { 27 | "is_private": false, 28 | "pull_count": 0, 29 | "star_count": 0 30 | }, 31 | "Parent": null 32 | }, 33 | "Permission": { 34 | "Value": "repo:admin", 35 | "Parent": null 36 | } 37 | } 38 | ], 39 | "UnboundedResources": null, 40 | "Metadata": { 41 | "Valid_Key": true 42 | } 43 | } -------------------------------------------------------------------------------- /pkg/analyzer/analyzers/dropbox/models.go: -------------------------------------------------------------------------------- 1 | package dropbox 2 | 3 | type scopeConfig struct { 4 | Scopes map[string]scope `json:"scopes"` 5 | } 6 | 7 | type scope struct { 8 | TestEndpoint string `json:"test_endpoint"` 9 | ImpliedScopes []string `json:"implied_scopes"` 10 | Actions []string `json:"actions"` 11 | } 12 | 13 | type account struct { 14 | AccountID string `json:"account_id"` 15 | Name name `json:"name"` 16 | Email string `json:"email"` 17 | EmailVerified bool `json:"email_verified"` 18 | Disabled bool `json:"disabled"` 19 | Country string `json:"country"` 20 | AccountType accountType `json:"account_type"` 21 | } 22 | 23 | type accountType struct { 24 | Tag string `json:".tag"` 25 | } 26 | 27 | type name struct { 28 | GivenName string `json:"given_name"` 29 | Surname string `json:"surname"` 30 | } 31 | 32 | type accountPermission struct { 33 | Name string 34 | Status PermissionStatus 35 | Actions []string 36 | } 37 | 38 | type secretInfo struct { 39 | Account account 40 | Permissions []accountPermission 41 | } 42 | -------------------------------------------------------------------------------- /pkg/analyzer/analyzers/dropbox/permissions.yaml: -------------------------------------------------------------------------------- 1 | permissions: 2 | - account_info.write 3 | - account_info.read 4 | - files.metadata.write 5 | - files.metadata.read 6 | - files.content.write 7 | - files.content.read 8 | - sharing.write 9 | - sharing.read 10 | - file_requests.write 11 | - file_requests.read 12 | - contacts.write 13 | - contacts.read 14 | - openid 15 | - profile 16 | - email 17 | -------------------------------------------------------------------------------- /pkg/analyzer/analyzers/elevenlabs/permissions.yaml: -------------------------------------------------------------------------------- 1 | permissions: 2 | - text_to_speech 3 | - speech_to_speech 4 | # - sound_generation 5 | - audio_isolation 6 | # - voice_generation 7 | - dubbing_read 8 | - dubbing_write 9 | - projects_read 10 | - projects_write 11 | - audio_native_read 12 | - audio_native_write 13 | - pronunciation_dictionaries_read 14 | - pronunciation_dictionaries_write 15 | - voices_read 16 | - voices_write 17 | - models_read 18 | # - models_write 19 | - speech_history_read 20 | - speech_history_write 21 | - user_read 22 | # - user_write 23 | - workspace_read 24 | - workspace_write 25 | -------------------------------------------------------------------------------- /pkg/analyzer/analyzers/fastly/permissions.yaml: -------------------------------------------------------------------------------- 1 | permissions: 2 | - global 3 | - global:read 4 | - purge_all 5 | - purge_select 6 | -------------------------------------------------------------------------------- /pkg/analyzer/analyzers/figma/endpoints.json: -------------------------------------------------------------------------------- 1 | { 2 | "files:read": { 3 | "url": "https://api.figma.com/v1/me", 4 | "method": "GET", 5 | "expected_status_code_with_scope": 200, 6 | "expected_status_code_without_scope": 403 7 | }, 8 | "library_analytics:read": { 9 | "url": "https://api.figma.com/v1/analytics/libraries/0/component/actions", 10 | "method": "GET", 11 | "expected_status_code_with_scope": 400, 12 | "expected_status_code_without_scope": 403 13 | }, 14 | "file_dev_resources:write": { 15 | "url": "https://api.figma.com/v1/dev_resources", 16 | "method": "POST", 17 | "expected_status_code_with_scope": 400, 18 | "expected_status_code_without_scope": 403 19 | }, 20 | "file_variables:read": { 21 | "url": "https://api.figma.com/v1/files/0/variables/published", 22 | "method": "GET", 23 | "expected_status_code_with_scope": 404, 24 | "expected_status_code_without_scope": 403 25 | }, 26 | "webhooks:write": { 27 | "url": "https://api.figma.com/v2/webhooks", 28 | "method": "POST", 29 | "expected_status_code_with_scope": 400, 30 | "expected_status_code_without_scope": 403 31 | } 32 | } -------------------------------------------------------------------------------- /pkg/analyzer/analyzers/figma/expected_output.json: -------------------------------------------------------------------------------- 1 | {"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} -------------------------------------------------------------------------------- /pkg/analyzer/analyzers/figma/models.go: -------------------------------------------------------------------------------- 1 | package figma 2 | 3 | type userInfo struct { 4 | ID string `json:"id"` 5 | Handle string `json:"handle"` 6 | ImgURL string `json:"img_url"` 7 | Email string `json:"email"` 8 | } 9 | 10 | type secretInfo struct { 11 | UserInfo userInfo 12 | Scopes map[Scope]ScopeStatus 13 | } 14 | 15 | type endpoint struct { 16 | URL string `json:"url"` 17 | Method string `json:"method"` 18 | ExpectedStatusCodeWithScope int `json:"expected_status_code_with_scope"` 19 | ExpectedStatusCodeWithoutScope int `json:"expected_status_code_without_scope"` 20 | } 21 | -------------------------------------------------------------------------------- /pkg/analyzer/analyzers/figma/permissions.yaml: -------------------------------------------------------------------------------- 1 | permissions: 2 | - files:read 3 | - file_variables:read 4 | - file_variables:write 5 | - file_comments:write 6 | - file_dev_resources:read 7 | - file_dev_resources:write 8 | - library_analytics:read 9 | - webhooks:write 10 | -------------------------------------------------------------------------------- /pkg/analyzer/analyzers/figma/requests.go: -------------------------------------------------------------------------------- 1 | package figma 2 | 3 | import ( 4 | "net/http" 5 | ) 6 | 7 | func callAPIEndpoint(client *http.Client, token string, endpoint endpoint) (*http.Response, error) { 8 | req, err := http.NewRequest(endpoint.Method, endpoint.URL, nil) 9 | if err != nil { 10 | return nil, err 11 | } 12 | req.Header.Set("X-FIGMA-TOKEN", token) 13 | resp, err := http.DefaultClient.Do(req) 14 | if err != nil { 15 | return nil, err 16 | } 17 | 18 | return resp, nil 19 | } 20 | -------------------------------------------------------------------------------- /pkg/analyzer/analyzers/github/classic/classic.yaml: -------------------------------------------------------------------------------- 1 | permissions: 2 | - repo 3 | - repo:status 4 | - repo_deployment 5 | - public_repo 6 | - repo:invite 7 | - security_events 8 | - workflow 9 | - write:packages 10 | - read:packages 11 | - delete:packages 12 | - admin:org 13 | - write:org 14 | - read:org 15 | - manage_runners:org 16 | - admin:public_key 17 | - write:public_key 18 | - read:public_key 19 | - admin:repo_hook 20 | - write:repo_hook 21 | - read:repo_hook 22 | - admin:org_hook 23 | - gist 24 | - notifications 25 | - user 26 | - read:user 27 | - user:email 28 | - user:follow 29 | - delete_repo 30 | - write:discussion 31 | - read:discussion 32 | - admin:enterprise 33 | - manage_runners:enterprise 34 | - manage_billing:enterprise 35 | - read:enterprise 36 | - audit_log 37 | - read:audit_log 38 | - codespace 39 | - codespace:secrets 40 | - copilot 41 | - manage_billing:copilot 42 | - project 43 | - read:project 44 | - admin:gpg_key 45 | - write:gpg_key 46 | - read:gpg_key 47 | - admin:ssh_signing_key 48 | - write:ssh_signing_key 49 | - read:ssh_signing_key 50 | -------------------------------------------------------------------------------- /pkg/analyzer/analyzers/github/finegrained/finegrained_test.go: -------------------------------------------------------------------------------- 1 | package finegrained 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | gh "github.com/google/go-github/v67/github" 8 | 9 | "github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers" 10 | analyzerCommon "github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers/github/common" 11 | "github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/config" 12 | "github.com/trufflesecurity/trufflehog/v3/pkg/common" 13 | "github.com/trufflesecurity/trufflehog/v3/pkg/context" 14 | ) 15 | 16 | func TestAnalyzer_Analyze(t *testing.T) { 17 | ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) 18 | defer cancel() 19 | 20 | analyzerSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "analyzers1") 21 | if err != nil { 22 | t.Fatalf("could not get test secrets from GCP: %s", err) 23 | } 24 | 25 | tests := []struct { 26 | name string 27 | key string 28 | wantErr bool 29 | }{ 30 | { 31 | name: "finegrained - github-allrepos-actionsRW-contentsRW-issuesRW", 32 | key: analyzerSecrets.MustGetField("GITHUB_FINEGRAINED_ALLREPOS_ACTIONS_RW_CONTENTS_RW_ISSUES_RW"), 33 | wantErr: false, 34 | }, 35 | } 36 | for _, tt := range tests { 37 | t.Run(tt.name, func(t *testing.T) { 38 | cfg := &config.Config{} 39 | key := tt.key 40 | client := gh.NewClient(analyzers.NewAnalyzeClient(cfg)).WithAuthToken(key) 41 | 42 | md, err := analyzerCommon.GetTokenMetadata(key, client) 43 | if err != nil { 44 | t.Fatalf("could not get token metadata: %s", err) 45 | } 46 | 47 | _, err = AnalyzeFineGrainedToken(client, md, cfg.Shallow) 48 | if (err != nil) != tt.wantErr { 49 | t.Errorf("Analyzer.Analyze() error = %v, wantErr %v", err, tt.wantErr) 50 | return 51 | } 52 | }) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /pkg/analyzer/analyzers/gitlab/expected_output.json: -------------------------------------------------------------------------------- 1 | {"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}} -------------------------------------------------------------------------------- /pkg/analyzer/analyzers/gitlab/permissions.yaml: -------------------------------------------------------------------------------- 1 | permissions: 2 | - api 3 | - read_user 4 | - read_api 5 | - read_repository 6 | - write_repository 7 | - read_registry 8 | - write_registry 9 | - sudo 10 | - admin_mode 11 | - create_runner 12 | - manage_runner 13 | - ai_features 14 | - k8s_proxy 15 | - read_service_ping 16 | -------------------------------------------------------------------------------- /pkg/analyzer/analyzers/groq/permissions.go: -------------------------------------------------------------------------------- 1 | // Code generated by go generate; DO NOT EDIT. 2 | package groq 3 | 4 | import "errors" 5 | 6 | type Permission int 7 | 8 | const ( 9 | Invalid Permission = iota 10 | FullAccess Permission = iota 11 | ) 12 | 13 | var ( 14 | PermissionStrings = map[Permission]string{ 15 | FullAccess: "full_access", 16 | } 17 | 18 | StringToPermission = map[string]Permission{ 19 | "full_access": FullAccess, 20 | } 21 | 22 | PermissionIDs = map[Permission]int{ 23 | FullAccess: 1, 24 | } 25 | 26 | IdToPermission = map[int]Permission{ 27 | 1: FullAccess, 28 | } 29 | ) 30 | 31 | // ToString converts a Permission enum to its string representation 32 | func (p Permission) ToString() (string, error) { 33 | if str, ok := PermissionStrings[p]; ok { 34 | return str, nil 35 | } 36 | return "", errors.New("invalid permission") 37 | } 38 | 39 | // ToID converts a Permission enum to its ID 40 | func (p Permission) ToID() (int, error) { 41 | if id, ok := PermissionIDs[p]; ok { 42 | return id, nil 43 | } 44 | return 0, errors.New("invalid permission") 45 | } 46 | 47 | // PermissionFromString converts a string representation to its Permission enum 48 | func PermissionFromString(s string) (Permission, error) { 49 | if p, ok := StringToPermission[s]; ok { 50 | return p, nil 51 | } 52 | return 0, errors.New("invalid permission string") 53 | } 54 | 55 | // PermissionFromID converts an ID to its Permission enum 56 | func PermissionFromID(id int) (Permission, error) { 57 | if p, ok := IdToPermission[id]; ok { 58 | return p, nil 59 | } 60 | return 0, errors.New("invalid permission ID") 61 | } 62 | -------------------------------------------------------------------------------- /pkg/analyzer/analyzers/groq/permissions.yaml: -------------------------------------------------------------------------------- 1 | permissions: 2 | - full_access # by default groq api key has full access 3 | -------------------------------------------------------------------------------- /pkg/analyzer/analyzers/huggingface/permissions.go: -------------------------------------------------------------------------------- 1 | // Code generated by go generate; DO NOT EDIT. 2 | package huggingface 3 | 4 | import "errors" 5 | 6 | type Permission int 7 | 8 | const ( 9 | Invalid Permission = iota 10 | Read Permission = iota 11 | Write Permission = iota 12 | ) 13 | 14 | var ( 15 | PermissionStrings = map[Permission]string{ 16 | Read: "read", 17 | Write: "write", 18 | } 19 | 20 | StringToPermission = map[string]Permission{ 21 | "read": Read, 22 | "write": Write, 23 | } 24 | 25 | PermissionIDs = map[Permission]int{ 26 | Read: 1, 27 | Write: 2, 28 | } 29 | 30 | IdToPermission = map[int]Permission{ 31 | 1: Read, 32 | 2: Write, 33 | } 34 | ) 35 | 36 | // ToString converts a Permission enum to its string representation 37 | func (p Permission) ToString() (string, error) { 38 | if str, ok := PermissionStrings[p]; ok { 39 | return str, nil 40 | } 41 | return "", errors.New("invalid permission") 42 | } 43 | 44 | // ToID converts a Permission enum to its ID 45 | func (p Permission) ToID() (int, error) { 46 | if id, ok := PermissionIDs[p]; ok { 47 | return id, nil 48 | } 49 | return 0, errors.New("invalid permission") 50 | } 51 | 52 | // PermissionFromString converts a string representation to its Permission enum 53 | func PermissionFromString(s string) (Permission, error) { 54 | if p, ok := StringToPermission[s]; ok { 55 | return p, nil 56 | } 57 | return 0, errors.New("invalid permission string") 58 | } 59 | 60 | // PermissionFromID converts an ID to its Permission enum 61 | func PermissionFromID(id int) (Permission, error) { 62 | if p, ok := IdToPermission[id]; ok { 63 | return p, nil 64 | } 65 | return 0, errors.New("invalid permission ID") 66 | } 67 | -------------------------------------------------------------------------------- /pkg/analyzer/analyzers/huggingface/permissions.yaml: -------------------------------------------------------------------------------- 1 | permissions: 2 | - read 3 | - write -------------------------------------------------------------------------------- /pkg/analyzer/analyzers/launchdarkly/permissions.yaml: -------------------------------------------------------------------------------- 1 | permissions: 2 | - admin 3 | - writer 4 | - reader 5 | - inlinepolicy 6 | - customroles -------------------------------------------------------------------------------- /pkg/analyzer/analyzers/mailchimp/permissions.yaml: -------------------------------------------------------------------------------- 1 | permissions: 2 | - invite_users 3 | - revoke_account_access 4 | - set_user_access_level 5 | - require_2_factor_authentication 6 | - change_billing_information 7 | - change_company_organization_name 8 | - add_or_access_api_keys 9 | - check_reconnect_integrations 10 | - referral_program 11 | - account_export 12 | - close_account 13 | - add_files_to_content_studio 14 | - opt_in_to_receive_emails_from_mailchimp 15 | - create_audiences 16 | - view_audiences 17 | - audience_export 18 | - audience_import 19 | - add_contacts 20 | - delete_contacts 21 | - view_segments 22 | - edit_audience_settings 23 | - archive_contacts 24 | - create_or_import_templates 25 | - edit_templates 26 | - create_emails 27 | - edit_emails 28 | - send_publish_emails 29 | - pause_unpublish_emails 30 | - delete_emails 31 | - submit_sms_marketing_application 32 | - create_send_sms_mms_messages 33 | - purchase_sms_credits 34 | - view_email_reports 35 | - view_sms_reports 36 | - view_abuse_reports 37 | - view_email_statistics 38 | - use_conversations 39 | - view_email_recipients 40 | - top_locations 41 | - email_contact_details 42 | - email_open_details 43 | - e_commerce_product_activity 44 | - domain_performance 45 | - create_your_website 46 | - publish_unpublish_your_website 47 | - view_report 48 | - create_a_landing_page 49 | - publish_unpublish_a_landing_page 50 | - replicate_a_landing_page 51 | - verify_a_domain 52 | - connect_a_domain 53 | - create_customer_journey 54 | - view_customer_journey 55 | - edit_customer_journey 56 | - turn_on_pause_turn_back_on 57 | - view_messages 58 | - leave_comments 59 | - send_messages 60 | - toggle_user_notifications 61 | - create_survey 62 | - edit_survey 63 | - publish_survey 64 | - delete_survey 65 | - create_form 66 | - edit_form 67 | - publish_form 68 | - delete_form 69 | -------------------------------------------------------------------------------- /pkg/analyzer/analyzers/mailgun/expected_output.json: -------------------------------------------------------------------------------- 1 | { 2 | "AnalyzerType": 8, 3 | "Bindings": [ 4 | { 5 | "Resource": { 6 | "Name": "sandbox19e49763d44e498e850589ea7d54bd82.mailgun.org", 7 | "FullyQualifiedName": "mailgun/6478cb31d026c112819856cd/sandbox19e49763d44e498e850589ea7d54bd82.mailgun.org", 8 | "Type": "domain", 9 | "Metadata": { 10 | "created_at": "Thu, 01 Jun 2023 16:45:37 GMT", 11 | "is_disabled": false, 12 | "state": "active", 13 | "type": "sandbox" 14 | }, 15 | "Parent": null 16 | }, 17 | "Permission": { 18 | "Value": "full_access", 19 | "Parent": null 20 | } 21 | } 22 | ], 23 | "UnboundedResources": null, 24 | "Metadata": null 25 | } -------------------------------------------------------------------------------- /pkg/analyzer/analyzers/mailgun/permissions.yaml: -------------------------------------------------------------------------------- 1 | permissions: 2 | - read 3 | - write 4 | - full_access 5 | -------------------------------------------------------------------------------- /pkg/analyzer/analyzers/monday/permissions.go: -------------------------------------------------------------------------------- 1 | // Code generated by go generate; DO NOT EDIT. 2 | package monday 3 | 4 | import "errors" 5 | 6 | type Permission int 7 | 8 | const ( 9 | Invalid Permission = iota 10 | FullAccess Permission = iota 11 | ) 12 | 13 | var ( 14 | PermissionStrings = map[Permission]string{ 15 | FullAccess: "full_access", 16 | } 17 | 18 | StringToPermission = map[string]Permission{ 19 | "full_access": FullAccess, 20 | } 21 | 22 | PermissionIDs = map[Permission]int{ 23 | FullAccess: 1, 24 | } 25 | 26 | IdToPermission = map[int]Permission{ 27 | 1: FullAccess, 28 | } 29 | ) 30 | 31 | // ToString converts a Permission enum to its string representation 32 | func (p Permission) ToString() (string, error) { 33 | if str, ok := PermissionStrings[p]; ok { 34 | return str, nil 35 | } 36 | return "", errors.New("invalid permission") 37 | } 38 | 39 | // ToID converts a Permission enum to its ID 40 | func (p Permission) ToID() (int, error) { 41 | if id, ok := PermissionIDs[p]; ok { 42 | return id, nil 43 | } 44 | return 0, errors.New("invalid permission") 45 | } 46 | 47 | // PermissionFromString converts a string representation to its Permission enum 48 | func PermissionFromString(s string) (Permission, error) { 49 | if p, ok := StringToPermission[s]; ok { 50 | return p, nil 51 | } 52 | return 0, errors.New("invalid permission string") 53 | } 54 | 55 | // PermissionFromID converts an ID to its Permission enum 56 | func PermissionFromID(id int) (Permission, error) { 57 | if p, ok := IdToPermission[id]; ok { 58 | return p, nil 59 | } 60 | return 0, errors.New("invalid permission ID") 61 | } 62 | -------------------------------------------------------------------------------- /pkg/analyzer/analyzers/monday/permissions.yaml: -------------------------------------------------------------------------------- 1 | permissions: 2 | - full_access 3 | -------------------------------------------------------------------------------- /pkg/analyzer/analyzers/monday/query.graphql: -------------------------------------------------------------------------------- 1 | { 2 | me { 3 | id 4 | name 5 | email 6 | title 7 | is_admin 8 | is_guest 9 | is_view_only 10 | is_pending 11 | is_verified 12 | teams { 13 | id 14 | name 15 | } 16 | } 17 | account { 18 | id 19 | name 20 | slug 21 | tier 22 | } 23 | users { 24 | email 25 | account { 26 | name 27 | id 28 | } 29 | } 30 | boards { 31 | id 32 | name 33 | state 34 | permissions 35 | groups { 36 | title 37 | id 38 | } 39 | columns { 40 | id 41 | title 42 | type 43 | } 44 | owners { 45 | id 46 | name 47 | } 48 | } 49 | docs { 50 | id 51 | object_id 52 | name 53 | created_by { 54 | id 55 | name 56 | } 57 | } 58 | folders { 59 | name 60 | id 61 | } 62 | tags { 63 | id 64 | name 65 | } 66 | teams { 67 | id 68 | name 69 | } 70 | workspaces { 71 | id 72 | name 73 | kind 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /pkg/analyzer/analyzers/mux/permissions.go: -------------------------------------------------------------------------------- 1 | // Code generated by go generate; DO NOT EDIT. 2 | package mux 3 | 4 | import "errors" 5 | 6 | type Permission int 7 | 8 | const ( 9 | Invalid Permission = iota 10 | Read Permission = iota 11 | FullAccess Permission = iota 12 | ) 13 | 14 | var ( 15 | PermissionStrings = map[Permission]string{ 16 | Read: "read", 17 | FullAccess: "full_access", 18 | } 19 | 20 | StringToPermission = map[string]Permission{ 21 | "read": Read, 22 | "full_access": FullAccess, 23 | } 24 | 25 | PermissionIDs = map[Permission]int{ 26 | Read: 1, 27 | FullAccess: 2, 28 | } 29 | 30 | IdToPermission = map[int]Permission{ 31 | 1: Read, 32 | 2: FullAccess, 33 | } 34 | ) 35 | 36 | // ToString converts a Permission enum to its string representation 37 | func (p Permission) ToString() (string, error) { 38 | if str, ok := PermissionStrings[p]; ok { 39 | return str, nil 40 | } 41 | return "", errors.New("invalid permission") 42 | } 43 | 44 | // ToID converts a Permission enum to its ID 45 | func (p Permission) ToID() (int, error) { 46 | if id, ok := PermissionIDs[p]; ok { 47 | return id, nil 48 | } 49 | return 0, errors.New("invalid permission") 50 | } 51 | 52 | // PermissionFromString converts a string representation to its Permission enum 53 | func PermissionFromString(s string) (Permission, error) { 54 | if p, ok := StringToPermission[s]; ok { 55 | return p, nil 56 | } 57 | return 0, errors.New("invalid permission string") 58 | } 59 | 60 | // PermissionFromID converts an ID to its Permission enum 61 | func PermissionFromID(id int) (Permission, error) { 62 | if p, ok := IdToPermission[id]; ok { 63 | return p, nil 64 | } 65 | return 0, errors.New("invalid permission ID") 66 | } 67 | -------------------------------------------------------------------------------- /pkg/analyzer/analyzers/mux/permissions.yaml: -------------------------------------------------------------------------------- 1 | permissions: 2 | - read 3 | - full_access 4 | -------------------------------------------------------------------------------- /pkg/analyzer/analyzers/mux/tests.json: -------------------------------------------------------------------------------- 1 | { 2 | "tests": [ 3 | { 4 | "resource_type": "video", 5 | "permission": "read", 6 | "endpoint": "/video/v1/assets?limit=1", 7 | "method": "GET", 8 | "valid_status_code": 200 9 | }, 10 | { 11 | "resource_type": "video", 12 | "permission": "write", 13 | "endpoint": "/video/v1/assets", 14 | "method": "POST", 15 | "valid_status_code": 400 16 | }, 17 | { 18 | "resource_type": "data", 19 | "permission": "read", 20 | "endpoint": "/data/v1/annotations?limit=1", 21 | "method": "GET", 22 | "valid_status_code": 200 23 | }, 24 | { 25 | "resource_type": "data", 26 | "permission": "write", 27 | "endpoint": "/data/v1/annotations", 28 | "method": "POST", 29 | "valid_status_code": 400 30 | }, 31 | { 32 | "resource_type": "system", 33 | "permission": "read", 34 | "endpoint": "/system/v1/signing-keys?limit=1", 35 | "method": "GET", 36 | "valid_status_code": 200 37 | }, 38 | { 39 | "resource_type": "system", 40 | "permission": "write", 41 | "endpoint": "/system/v1/signing-keys", 42 | "method": "DELETE", 43 | "valid_status_code": 400 44 | } 45 | ] 46 | } -------------------------------------------------------------------------------- /pkg/analyzer/analyzers/mysql/permissions.yaml: -------------------------------------------------------------------------------- 1 | permissions: 2 | - alter 3 | - alter_routine 4 | - allow_nonexistent_definer 5 | - application_password_admin 6 | - audit_abort_exempt 7 | - audit_admin 8 | - authentication_policy_admin 9 | - backup_admin 10 | - binlog_admin 11 | - binlog_encryption_admin 12 | - clone_admin 13 | - connection_admin 14 | - create 15 | - create_role 16 | - create_routine 17 | - create_tablespace 18 | - create_temporary_tables 19 | - create_user 20 | - create_view 21 | - delete 22 | - drop 23 | - drop_role 24 | - encryption_key_admin 25 | - event 26 | - execute 27 | - file 28 | - firewall_admin 29 | - firewall_exempt 30 | - firewall_user 31 | - flush_optimizer_costs 32 | - flush_status 33 | - flush_tables 34 | - flush_user_resources 35 | - grant_option 36 | - group_replication_admin 37 | - group_replication_stream 38 | - index 39 | - innodb_redo_log_archive 40 | - innodb_redo_log_enable 41 | - insert 42 | - locking_tables 43 | - masking_dictionaries_admin 44 | - ndb_stored_user 45 | - passwordless_user_admin 46 | - persist_ro_variables_admin 47 | - process 48 | - proxy 49 | - references 50 | - reload 51 | - replication_applier 52 | - replication_client 53 | - replication_slave 54 | - replication_slave_admin 55 | - resource_group_admin 56 | - resource_group_user 57 | - role_admin 58 | - select 59 | - sensitive_variables_observer 60 | - service_connection_admin 61 | - session_variables_admin 62 | - set_any_definer 63 | - set_user_id 64 | - show_databases 65 | - show_routine 66 | - show_view 67 | - shutdown 68 | - skip_query_rewrite 69 | - super 70 | - system_user 71 | - system_variables_admin 72 | - table_encryption_admin 73 | - telemetry_log_admin 74 | - tp_connection_admin 75 | - transaction_gtid_tag 76 | - trigger 77 | - update 78 | - usage 79 | - version_token_admin 80 | - xa_recover_admin 81 | -------------------------------------------------------------------------------- /pkg/analyzer/analyzers/netlify/permissions.go: -------------------------------------------------------------------------------- 1 | // Code generated by go generate; DO NOT EDIT. 2 | package netlify 3 | 4 | import "errors" 5 | 6 | type Permission int 7 | 8 | const ( 9 | Invalid Permission = iota 10 | FullAccess Permission = iota 11 | ) 12 | 13 | var ( 14 | PermissionStrings = map[Permission]string{ 15 | FullAccess: "full_access", 16 | } 17 | 18 | StringToPermission = map[string]Permission{ 19 | "full_access": FullAccess, 20 | } 21 | 22 | PermissionIDs = map[Permission]int{ 23 | FullAccess: 1, 24 | } 25 | 26 | IdToPermission = map[int]Permission{ 27 | 1: FullAccess, 28 | } 29 | ) 30 | 31 | // ToString converts a Permission enum to its string representation 32 | func (p Permission) ToString() (string, error) { 33 | if str, ok := PermissionStrings[p]; ok { 34 | return str, nil 35 | } 36 | return "", errors.New("invalid permission") 37 | } 38 | 39 | // ToID converts a Permission enum to its ID 40 | func (p Permission) ToID() (int, error) { 41 | if id, ok := PermissionIDs[p]; ok { 42 | return id, nil 43 | } 44 | return 0, errors.New("invalid permission") 45 | } 46 | 47 | // PermissionFromString converts a string representation to its Permission enum 48 | func PermissionFromString(s string) (Permission, error) { 49 | if p, ok := StringToPermission[s]; ok { 50 | return p, nil 51 | } 52 | return 0, errors.New("invalid permission string") 53 | } 54 | 55 | // PermissionFromID converts an ID to its Permission enum 56 | func PermissionFromID(id int) (Permission, error) { 57 | if p, ok := IdToPermission[id]; ok { 58 | return p, nil 59 | } 60 | return 0, errors.New("invalid permission ID") 61 | } 62 | -------------------------------------------------------------------------------- /pkg/analyzer/analyzers/netlify/permissions.yaml: -------------------------------------------------------------------------------- 1 | permissions: 2 | - full_access -------------------------------------------------------------------------------- /pkg/analyzer/analyzers/ngrok/permissions.go: -------------------------------------------------------------------------------- 1 | // Code generated by go generate; DO NOT EDIT. 2 | package ngrok 3 | 4 | import "errors" 5 | 6 | type Permission int 7 | 8 | const ( 9 | Invalid Permission = iota 10 | FullAccess Permission = iota 11 | ) 12 | 13 | var ( 14 | PermissionStrings = map[Permission]string{ 15 | FullAccess: "full_access", 16 | } 17 | 18 | StringToPermission = map[string]Permission{ 19 | "full_access": FullAccess, 20 | } 21 | 22 | PermissionIDs = map[Permission]int{ 23 | FullAccess: 1, 24 | } 25 | 26 | IdToPermission = map[int]Permission{ 27 | 1: FullAccess, 28 | } 29 | ) 30 | 31 | // ToString converts a Permission enum to its string representation 32 | func (p Permission) ToString() (string, error) { 33 | if str, ok := PermissionStrings[p]; ok { 34 | return str, nil 35 | } 36 | return "", errors.New("invalid permission") 37 | } 38 | 39 | // ToID converts a Permission enum to its ID 40 | func (p Permission) ToID() (int, error) { 41 | if id, ok := PermissionIDs[p]; ok { 42 | return id, nil 43 | } 44 | return 0, errors.New("invalid permission") 45 | } 46 | 47 | // PermissionFromString converts a string representation to its Permission enum 48 | func PermissionFromString(s string) (Permission, error) { 49 | if p, ok := StringToPermission[s]; ok { 50 | return p, nil 51 | } 52 | return 0, errors.New("invalid permission string") 53 | } 54 | 55 | // PermissionFromID converts an ID to its Permission enum 56 | func PermissionFromID(id int) (Permission, error) { 57 | if p, ok := IdToPermission[id]; ok { 58 | return p, nil 59 | } 60 | return 0, errors.New("invalid permission ID") 61 | } 62 | -------------------------------------------------------------------------------- /pkg/analyzer/analyzers/ngrok/permissions.yaml: -------------------------------------------------------------------------------- 1 | permissions: 2 | - full_access 3 | -------------------------------------------------------------------------------- /pkg/analyzer/analyzers/notion/expected_output.json: -------------------------------------------------------------------------------- 1 | {"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} -------------------------------------------------------------------------------- /pkg/analyzer/analyzers/notion/permissions.yaml: -------------------------------------------------------------------------------- 1 | permissions: 2 | - read_content 3 | - update_content 4 | - insert_content 5 | - read_comments 6 | - insert_comments 7 | - read_users_with_email 8 | - read_users_without_email -------------------------------------------------------------------------------- /pkg/analyzer/analyzers/notion/scopes.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "read_content", 4 | "test": { 5 | "endpoint": "https://api.notion.com/v1/pages/`nowaythiscanexist", 6 | "method": "GET", 7 | "valid_status_code": [400], 8 | "invalid_status_code": [403] 9 | } 10 | }, 11 | { 12 | "name": "update_content", 13 | "test": { 14 | "endpoint": "https://api.notion.com/v1/pages/`nowaythiscanexist", 15 | "method": "PATCH", 16 | "valid_status_code": [400], 17 | "invalid_status_code": [403] 18 | } 19 | }, 20 | { 21 | "name": "insert_content", 22 | "test": { 23 | "endpoint": "https://api.notion.com/v1/pages", 24 | "method": "POST", 25 | "valid_status_code": [400], 26 | "invalid_status_code": [403] 27 | } 28 | }, 29 | { 30 | "name": "read_comments", 31 | "test": { 32 | "endpoint": "https://api.notion.com/v1/comments", 33 | "method": "GET", 34 | "valid_status_code": [400], 35 | "invalid_status_code": [403] 36 | } 37 | }, 38 | { 39 | "name": "insert_comments", 40 | "test": { 41 | "endpoint": "https://api.notion.com/v1/comments", 42 | "method": "POST", 43 | "valid_status_code": [400], 44 | "invalid_status_code": [403] 45 | } 46 | } 47 | ] -------------------------------------------------------------------------------- /pkg/analyzer/analyzers/openai/permissions.yaml: -------------------------------------------------------------------------------- 1 | permissions: 2 | - models:read 3 | - model_capabilities:write 4 | - assistants:read 5 | - assistants:write 6 | - threads:read 7 | - threads:write 8 | - fine_tuning:read 9 | - fine_tuning:write 10 | - files:read 11 | - files:write 12 | -------------------------------------------------------------------------------- /pkg/analyzer/analyzers/opsgenie/permissions.yaml: -------------------------------------------------------------------------------- 1 | permissions: 2 | - configuration_access 3 | - read 4 | - delete 5 | - create_and_update -------------------------------------------------------------------------------- /pkg/analyzer/analyzers/opsgenie/scopes.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "configuration_access", 4 | "test": { 5 | "endpoint": "https://api.opsgenie.com/v2/account", 6 | "method": "GET", 7 | "valid_status_code": [200], 8 | "invalid_status_code": [403] 9 | } 10 | }, 11 | { 12 | "name": "read", 13 | "test": { 14 | "endpoint": "https://api.opsgenie.com/v2/alerts", 15 | "method": "GET", 16 | "valid_status_code": [200], 17 | "invalid_status_code": [403] 18 | } 19 | }, 20 | { 21 | "name": "delete", 22 | "test": { 23 | "endpoint": "https://api.opsgenie.com/v2/alerts/`nowaythiscanexist", 24 | "method": "DELETE", 25 | "valid_status_code": [202], 26 | "invalid_status_code": [403] 27 | } 28 | }, 29 | { 30 | "name": "create_and_update", 31 | "test": { 32 | "endpoint": "https://api.opsgenie.com/v2/alerts/`nowaycanthisexist/message", 33 | "method": "PUT", 34 | "valid_status_code": [400], 35 | "invalid_status_code": [403] 36 | } 37 | } 38 | ] -------------------------------------------------------------------------------- /pkg/analyzer/analyzers/plaid/models.go: -------------------------------------------------------------------------------- 1 | package plaid 2 | 3 | type account struct { 4 | AccountID string `json:"account_id"` 5 | Name string `json:"name"` 6 | OfficialName string `json:"official_name"` 7 | Subtype string `json:"subtype"` 8 | Type string `json:"type"` 9 | } 10 | 11 | type item struct { 12 | Products []string `json:"products"` 13 | ItemID string `json:"item_id"` 14 | } 15 | 16 | type accountsResponse struct { 17 | Accounts []account `json:"accounts"` 18 | Item item `json:"item"` 19 | } 20 | 21 | type secretInfo struct { 22 | Item item 23 | Accounts []account 24 | Environment string 25 | } 26 | -------------------------------------------------------------------------------- /pkg/analyzer/analyzers/plaid/permissions.go: -------------------------------------------------------------------------------- 1 | // Code generated by go generate; DO NOT EDIT. 2 | package plaid 3 | 4 | import "errors" 5 | 6 | type Permission int 7 | 8 | const ( 9 | Invalid Permission = iota 10 | Read Permission = iota 11 | Write Permission = iota 12 | ) 13 | 14 | var ( 15 | PermissionStrings = map[Permission]string{ 16 | Read: "read", 17 | Write: "write", 18 | } 19 | 20 | StringToPermission = map[string]Permission{ 21 | "read": Read, 22 | "write": Write, 23 | } 24 | 25 | PermissionIDs = map[Permission]int{ 26 | Read: 1, 27 | Write: 2, 28 | } 29 | 30 | IdToPermission = map[int]Permission{ 31 | 1: Read, 32 | 2: Write, 33 | } 34 | ) 35 | 36 | // ToString converts a Permission enum to its string representation 37 | func (p Permission) ToString() (string, error) { 38 | if str, ok := PermissionStrings[p]; ok { 39 | return str, nil 40 | } 41 | return "", errors.New("invalid permission") 42 | } 43 | 44 | // ToID converts a Permission enum to its ID 45 | func (p Permission) ToID() (int, error) { 46 | if id, ok := PermissionIDs[p]; ok { 47 | return id, nil 48 | } 49 | return 0, errors.New("invalid permission") 50 | } 51 | 52 | // PermissionFromString converts a string representation to its Permission enum 53 | func PermissionFromString(s string) (Permission, error) { 54 | if p, ok := StringToPermission[s]; ok { 55 | return p, nil 56 | } 57 | return 0, errors.New("invalid permission string") 58 | } 59 | 60 | // PermissionFromID converts an ID to its Permission enum 61 | func PermissionFromID(id int) (Permission, error) { 62 | if p, ok := IdToPermission[id]; ok { 63 | return p, nil 64 | } 65 | return 0, errors.New("invalid permission ID") 66 | } 67 | -------------------------------------------------------------------------------- /pkg/analyzer/analyzers/plaid/permissions.yaml: -------------------------------------------------------------------------------- 1 | permissions: 2 | - read 3 | - write 4 | -------------------------------------------------------------------------------- /pkg/analyzer/analyzers/planetscale/permissions.yaml: -------------------------------------------------------------------------------- 1 | permissions: 2 | - read_organization 3 | - read_invoices 4 | - read_databases 5 | - read_audit_logs 6 | - create_databases 7 | - delete_databases 8 | - read_oauth_applications 9 | - write_oauth_tokens 10 | - read_oauth_tokens 11 | - delete_oauth_tokens 12 | - read_database 13 | - write_database 14 | - delete_database 15 | - read_branch 16 | - create_branch 17 | - delete_branch 18 | - delete_branch_password 19 | - delete_production_branch 20 | - delete_production_branch_password 21 | - read_deploy_request 22 | - create_deploy_request 23 | - approve_deploy_request 24 | - connect_branch 25 | - connect_production_branch 26 | - read_comment 27 | - create_comment 28 | - restore_backup 29 | - write_backups 30 | - read_backups 31 | - delete_backups 32 | - restore_production_branch_backup 33 | - delete_production_branch_backups -------------------------------------------------------------------------------- /pkg/analyzer/analyzers/postgres/permissions.yaml: -------------------------------------------------------------------------------- 1 | permissions: 2 | - bypass_rls 3 | - connect 4 | - create 5 | - create_db 6 | - create_role 7 | - delete 8 | - inheritance_of_privs 9 | - insert 10 | - login 11 | - references 12 | - replication 13 | - select 14 | - superuser 15 | - temp 16 | - trigger 17 | - truncate 18 | - update 19 | -------------------------------------------------------------------------------- /pkg/analyzer/analyzers/posthog/permissions.yaml: -------------------------------------------------------------------------------- 1 | permissions: 2 | - action:read 3 | - action:write 4 | - activity_log:read 5 | - activity_log:write 6 | - annotation:read 7 | - annotation:write 8 | - batch_export:read 9 | - batch_export:write 10 | - cohort:read 11 | - cohort:write 12 | - dashboard:read 13 | - dashboard:write 14 | - dashboard_template:read 15 | - dashboard_template:write 16 | - early_access_feature:read 17 | - early_access_feature:write 18 | - event_definition:read 19 | - event_definition:write 20 | - error_tracking:read 21 | - error_tracking:write 22 | - experiment:read 23 | - experiment:write 24 | - export:read 25 | - export:write 26 | - feature_flag:read 27 | - feature_flag:write 28 | - group:read 29 | - group:write 30 | - hog_function:read 31 | - hog_function:write 32 | - insight:read 33 | - insight:write 34 | - notebook:read 35 | - notebook:write 36 | - organization:read 37 | - organization:write 38 | - organization_member:read 39 | - organization_member:write 40 | - person:read 41 | - person:write 42 | - plugin:read 43 | - plugin:write 44 | - project:read 45 | - project:write 46 | - property_definition:read 47 | - property_definition:write 48 | - query:read 49 | - session_recording:read 50 | - session_recording:write 51 | - session_recording_playlist:read 52 | - session_recording_playlist:write 53 | - sharing_configuration:read 54 | - sharing_configuration:write 55 | - subscription:read 56 | - subscription:write 57 | - survey:read 58 | - survey:write 59 | - user:read 60 | - webhook:read 61 | - webhook:write 62 | -------------------------------------------------------------------------------- /pkg/analyzer/analyzers/postman/permissions.yaml: -------------------------------------------------------------------------------- 1 | permissions: 2 | - user:add 3 | - user:remove 4 | - team_admin:manage 5 | - team_developers:manage 6 | - sso:manage 7 | - custom_domain:add 8 | - custom_domain:edit 9 | - custom_domain:remove 10 | - audit_logs:view 11 | - usage_data:view 12 | - billing_members:manage 13 | - payment:manage 14 | - plan:update 15 | - team_workspaces:view 16 | - team_workspaces:create 17 | - team_public_profile:enable 18 | - team_private_api_network:manage 19 | - parterner_workspace:view 20 | - parterner_workspace:manage 21 | - parterner_workspace_visibility:manage 22 | - partners:manage 23 | - flow:add 24 | - flow:edit 25 | - flow:run 26 | - flow:publish 27 | 28 | -------------------------------------------------------------------------------- /pkg/analyzer/analyzers/privatekey/expected_output.json: -------------------------------------------------------------------------------- 1 | {"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} -------------------------------------------------------------------------------- /pkg/analyzer/analyzers/privatekey/permissions.yaml: -------------------------------------------------------------------------------- 1 | permissions: 2 | # TLS: 3 | # KeyUsuage: https://datatracker.ietf.org/doc/html/rfc5280#section-4.2.1.3 4 | # ExtendedKeyUsage: https://datatracker.ietf.org/doc/html/rfc5280#section-4.2.1.12 5 | - DigitalSignature 6 | - NonRepudiation 7 | - KeyEncipherment 8 | - DataEncipherment 9 | - KeyAgreement 10 | - CertificateSigning 11 | - CRLSigning 12 | - EncipherOnly 13 | - DecipherOnly 14 | - ServerAuth 15 | - ClientAuth 16 | - CodeSigning 17 | - EmailProtection 18 | - TimeStamping 19 | - OCSPSigning 20 | 21 | # Github/Gitlab 22 | - Clone 23 | - Push -------------------------------------------------------------------------------- /pkg/analyzer/analyzers/shopify/permissions.yaml: -------------------------------------------------------------------------------- 1 | permissions: 2 | - read 3 | - write 4 | - full_access 5 | -------------------------------------------------------------------------------- /pkg/analyzer/analyzers/sourcegraph/permissions.go: -------------------------------------------------------------------------------- 1 | // Code generated by go generate; DO NOT EDIT. 2 | package sourcegraph 3 | 4 | import "errors" 5 | 6 | type Permission int 7 | 8 | const ( 9 | NoAccess Permission = iota 10 | UserRead Permission = iota 11 | SiteAdminFull Permission = iota 12 | ) 13 | 14 | var ( 15 | PermissionStrings = map[Permission]string{ 16 | UserRead: "user:read", 17 | SiteAdminFull: "site_admin:full", 18 | } 19 | 20 | StringToPermission = map[string]Permission{ 21 | "user:read": UserRead, 22 | "site_admin:full": SiteAdminFull, 23 | } 24 | 25 | PermissionIDs = map[Permission]int{ 26 | UserRead: 0, 27 | SiteAdminFull: 1, 28 | } 29 | 30 | IdToPermission = map[int]Permission{ 31 | 0: UserRead, 32 | 1: SiteAdminFull, 33 | } 34 | ) 35 | 36 | // ToString converts a Permission enum to its string representation 37 | func (p Permission) ToString() (string, error) { 38 | if str, ok := PermissionStrings[p]; ok { 39 | return str, nil 40 | } 41 | return "", errors.New("invalid permission") 42 | } 43 | 44 | // ToID converts a Permission enum to its ID 45 | func (p Permission) ToID() (int, error) { 46 | if id, ok := PermissionIDs[p]; ok { 47 | return id, nil 48 | } 49 | return 0, errors.New("invalid permission") 50 | } 51 | 52 | // PermissionFromString converts a string representation to its Permission enum 53 | func PermissionFromString(s string) (Permission, error) { 54 | if p, ok := StringToPermission[s]; ok { 55 | return p, nil 56 | } 57 | return 0, errors.New("invalid permission string") 58 | } 59 | 60 | // PermissionFromID converts an ID to its Permission enum 61 | func PermissionFromID(id int) (Permission, error) { 62 | if p, ok := IdToPermission[id]; ok { 63 | return p, nil 64 | } 65 | return 0, errors.New("invalid permission ID") 66 | } 67 | -------------------------------------------------------------------------------- /pkg/analyzer/analyzers/sourcegraph/permissions.yaml: -------------------------------------------------------------------------------- 1 | permissions: 2 | - user:read 3 | - site_admin:full 4 | -------------------------------------------------------------------------------- /pkg/analyzer/analyzers/square/permissions.yaml: -------------------------------------------------------------------------------- 1 | permissions: 2 | - bank_accounts_read 3 | - appointments_write 4 | - appointments_all_write 5 | - appointments_read 6 | - appointments_all_read 7 | - appointments_business_settings_read 8 | - payments_read 9 | - payments_write 10 | - cash_drawer_read 11 | - items_write 12 | - items_read 13 | - orders_write 14 | - orders_read 15 | - customers_write 16 | - customers_read 17 | - device_credential_management 18 | - devices_read 19 | - disputes_write 20 | - disputes_read 21 | - employees_read 22 | - giftcards_read 23 | - giftcards_write 24 | - inventory_write 25 | - inventory_read 26 | - invoices_write 27 | - invoices_read 28 | - timecards_settings_write 29 | - timecards_write 30 | - timecards_settings_read 31 | - timecards_read 32 | - merchant_profile_write 33 | - merchant_profile_read 34 | - loyalty_read 35 | - loyalty_write 36 | - payments_write_in_person 37 | - payments_write_shared_onfile 38 | - payments_write_additional_recipients 39 | - payouts_read 40 | - online_store_site_read 41 | - online_store_snippets_write 42 | - online_store_snippets_read 43 | - subscriptions_write 44 | - subscriptions_read 45 | -------------------------------------------------------------------------------- /pkg/analyzer/analyzers/twilio/permissions.yaml: -------------------------------------------------------------------------------- 1 | permissions: 2 | - account_management:read 3 | - account_management:write 4 | - subaccount_configuration:read 5 | - subaccount_configuration:write 6 | - key_management:read 7 | - key_management:write 8 | - service_verification:read 9 | - service_verification:write 10 | - sms:read 11 | - sms:write 12 | - voice:read 13 | - voice:write 14 | - messaging:read 15 | - messaging:write 16 | - call_management:read 17 | - call_management:write 18 | -------------------------------------------------------------------------------- /pkg/analyzer/config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | // TODO: separate CLI configuration from analysis configuration. 4 | type Config struct { 5 | LoggingEnabled bool 6 | LogFile string 7 | ShowAll bool 8 | // Limit API calls when enumerating permissions. 9 | Shallow bool 10 | } 11 | -------------------------------------------------------------------------------- /pkg/buffers/buffer/metrics.go: -------------------------------------------------------------------------------- 1 | package buffer 2 | 3 | import ( 4 | "github.com/prometheus/client_golang/prometheus" 5 | "github.com/prometheus/client_golang/prometheus/promauto" 6 | 7 | "github.com/trufflesecurity/trufflehog/v3/pkg/common" 8 | ) 9 | 10 | var ( 11 | growCount = promauto.NewCounter(prometheus.CounterOpts{ 12 | Namespace: common.MetricsNamespace, 13 | Subsystem: common.MetricsSubsystem, 14 | Name: "grow_count", 15 | Help: "Total number of times buffers in the pool have grown.", 16 | }) 17 | 18 | growAmount = promauto.NewCounter(prometheus.CounterOpts{ 19 | Namespace: common.MetricsNamespace, 20 | Subsystem: common.MetricsSubsystem, 21 | Name: "grow_amount", 22 | Help: "Total amount of bytes buffers in the pool have grown by.", 23 | }) 24 | 25 | checkoutDurationTotal = promauto.NewCounter(prometheus.CounterOpts{ 26 | Namespace: common.MetricsNamespace, 27 | Subsystem: common.MetricsSubsystem, 28 | Name: "checkout_duration_total_us", 29 | Help: "Total duration in microseconds of Buffer checkouts.", 30 | }) 31 | 32 | checkoutDuration = promauto.NewHistogram(prometheus.HistogramOpts{ 33 | Namespace: common.MetricsNamespace, 34 | Subsystem: common.MetricsSubsystem, 35 | Name: "checkout_duration_us", 36 | Help: "Duration in microseconds of Buffer checkouts.", 37 | Buckets: prometheus.ExponentialBuckets(10, 10, 7), 38 | }) 39 | 40 | totalBufferLength = promauto.NewGauge(prometheus.GaugeOpts{ 41 | Namespace: common.MetricsNamespace, 42 | Subsystem: common.MetricsSubsystem, 43 | Name: "total_buffer_length", 44 | Help: "Total length of all buffers combined.", 45 | }) 46 | 47 | totalBufferSize = promauto.NewGauge(prometheus.GaugeOpts{ 48 | Namespace: common.MetricsNamespace, 49 | Subsystem: common.MetricsSubsystem, 50 | Name: "total_buffer_size", 51 | Help: "Total size of all buffers combined.", 52 | }) 53 | ) 54 | -------------------------------------------------------------------------------- /pkg/buffers/pool/metrics.go: -------------------------------------------------------------------------------- 1 | package pool 2 | 3 | import ( 4 | "github.com/prometheus/client_golang/prometheus" 5 | "github.com/prometheus/client_golang/prometheus/promauto" 6 | 7 | "github.com/trufflesecurity/trufflehog/v3/pkg/common" 8 | ) 9 | 10 | var ( 11 | activeBufferCount = promauto.NewGauge(prometheus.GaugeOpts{ 12 | Namespace: common.MetricsNamespace, 13 | Subsystem: common.MetricsSubsystem, 14 | Name: "active_buffer_count", 15 | Help: "Current number of active buffers.", 16 | }) 17 | 18 | bufferCount = promauto.NewGauge(prometheus.GaugeOpts{ 19 | Namespace: common.MetricsNamespace, 20 | Subsystem: common.MetricsSubsystem, 21 | Name: "buffer_count", 22 | Help: "Total number of buffers managed by the pool.", 23 | }) 24 | 25 | shrinkCount = promauto.NewCounter(prometheus.CounterOpts{ 26 | Namespace: common.MetricsNamespace, 27 | Subsystem: common.MetricsSubsystem, 28 | Name: "shrink_count", 29 | Help: "Total number of times buffers in the pool have shrunk.", 30 | }) 31 | 32 | shrinkAmount = promauto.NewCounter(prometheus.CounterOpts{ 33 | Namespace: common.MetricsNamespace, 34 | Subsystem: common.MetricsSubsystem, 35 | Name: "shrink_amount", 36 | Help: "Total amount of bytes buffers in the pool have shrunk by.", 37 | }) 38 | 39 | checkoutCount = promauto.NewCounter(prometheus.CounterOpts{ 40 | Namespace: common.MetricsNamespace, 41 | Subsystem: common.MetricsSubsystem, 42 | Name: "checkout_count", 43 | Help: "Total number of Buffer checkouts.", 44 | }) 45 | ) 46 | -------------------------------------------------------------------------------- /pkg/cache/cache.go: -------------------------------------------------------------------------------- 1 | // Package cache provides an interface which can be implemented by different cache types. 2 | package cache 3 | 4 | // Cache is used to store key/value pairs. 5 | type Cache[T any] interface { 6 | // Set stores the given key/value pair. 7 | Set(key string, val T) 8 | // Get returns the value for the given key and a boolean indicating if the key was found. 9 | Get(key string) (T, bool) 10 | // Exists returns true if the given key exists in the cache. 11 | Exists(key string) bool 12 | // Delete the given key from the cache. 13 | Delete(key string) 14 | // Clear all key/value pairs from the cache. 15 | Clear() 16 | // Count the number of key/value pairs in the cache. 17 | Count() int 18 | // Keys returns all keys in the cache. 19 | Keys() []string 20 | // Values returns all values in the cache. 21 | Values() []T 22 | // Contents returns all keys in the cache encoded as a string. 23 | Contents() string 24 | } 25 | -------------------------------------------------------------------------------- /pkg/channelmetrics/noopcollector.go: -------------------------------------------------------------------------------- 1 | package channelmetrics 2 | 3 | import "time" 4 | 5 | // noopCollector is a default implementation of the MetricsCollector interface 6 | // for internal package use only. 7 | type noopCollector struct{} 8 | 9 | func (noopCollector) RecordProduceDuration(duration time.Duration) {} 10 | func (noopCollector) RecordConsumeDuration(duration time.Duration) {} 11 | func (noopCollector) RecordChannelLen(size int) {} 12 | func (noopCollector) RecordChannelCap(capacity int) {} 13 | -------------------------------------------------------------------------------- /pkg/cleantemp/cleantemp_test.go: -------------------------------------------------------------------------------- 1 | package cleantemp 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | "testing" 7 | 8 | "github.com/mitchellh/go-ps" 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | func TestExecName(t *testing.T) { 13 | executablePath, err := os.Executable() 14 | assert.Nil(t, err) 15 | execName := filepath.Base(executablePath) 16 | assert.Equal(t, "cleantemp.test", execName) 17 | 18 | procs, err := ps.Processes() 19 | assert.Nil(t, err) 20 | assert.NotEmpty(t, procs) 21 | 22 | found := false 23 | for _, proc := range procs { 24 | if proc.Executable() == execName { 25 | found = true 26 | } 27 | } 28 | 29 | assert.True(t, found) 30 | } 31 | -------------------------------------------------------------------------------- /pkg/common/context.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import "context" 4 | 5 | // ChannelClosedErr indicates that a read was performed from a closed channel. 6 | type ChannelClosedErr struct{} 7 | 8 | func (ChannelClosedErr) Error() string { return "channel is closed" } 9 | 10 | func IsDone(ctx context.Context) bool { 11 | select { 12 | case <-ctx.Done(): 13 | return true 14 | default: 15 | return false 16 | } 17 | } 18 | 19 | // CancellableWrite blocks on writing the item to the channel but can be 20 | // cancelled by the context. If both the context is cancelled and the channel 21 | // write would succeed, either operation will be performed randomly, however 22 | // priority is given to context cancellation. 23 | func CancellableWrite[T any](ctx context.Context, ch chan<- T, item T) error { 24 | select { 25 | case <-ctx.Done(): // priority to context cancellation 26 | return ctx.Err() 27 | default: 28 | select { 29 | case <-ctx.Done(): 30 | return ctx.Err() 31 | case ch <- item: 32 | return nil 33 | } 34 | } 35 | } 36 | 37 | // CancellableRead blocks on receiving an item from the channel but can be 38 | // cancelled by the context. If the channel is closed, a ChannelClosedErr is 39 | // returned. If both the context is cancelled and the channel read would 40 | // succeed, either operation will be performed randomly, however priority is 41 | // given to context cancellation. 42 | func CancellableRead[T any](ctx context.Context, ch <-chan T) (T, error) { 43 | var zero T // zero value of type T 44 | 45 | select { 46 | case <-ctx.Done(): // priority to context cancellation 47 | return zero, ctx.Err() 48 | default: 49 | select { 50 | case <-ctx.Done(): 51 | return zero, ctx.Err() 52 | case item, ok := <-ch: 53 | if !ok { 54 | return item, ChannelClosedErr{} 55 | } 56 | return item, nil 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /pkg/common/depaware.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | _ "github.com/tailscale/depaware/depaware" 5 | ) 6 | -------------------------------------------------------------------------------- /pkg/common/export_error.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | // ExportError is an implementation of error that can be JSON marshalled. It 4 | // must be a public exported type for this reason. 5 | type ExportError string 6 | 7 | func (e ExportError) Error() string { return string(e) } 8 | 9 | // ExportErrors converts a list of errors into []ExportError. 10 | func ExportErrors(errs ...error) []error { 11 | output := make([]error, 0, len(errs)) 12 | for _, err := range errs { 13 | output = append(output, ExportError(err.Error())) 14 | } 15 | return output 16 | } 17 | -------------------------------------------------------------------------------- /pkg/common/metrics.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | const ( 4 | // MetricsNamespace is the namespace for all metrics. 5 | MetricsNamespace = "trufflehog" 6 | // MetricsSubsystem is the subsystem for all metrics. 7 | MetricsSubsystem = "scanner" 8 | ) 9 | -------------------------------------------------------------------------------- /pkg/common/recover.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "runtime/debug" 7 | "time" 8 | 9 | "github.com/getsentry/sentry-go" 10 | 11 | "github.com/trufflesecurity/trufflehog/v3/pkg/context" 12 | ) 13 | 14 | // Recover handles panics and reports to Sentry. 15 | func Recover(ctx context.Context) { 16 | if err := recover(); err != nil { 17 | panicStack := string(debug.Stack()) 18 | if eventID := sentry.CurrentHub().Recover(err); eventID != nil { 19 | ctx.Logger().Info("panic captured", "event_id", *eventID) 20 | } 21 | ctx.Logger().Error(fmt.Errorf("panic"), panicStack, 22 | "recover", err, 23 | ) 24 | if !sentry.Flush(time.Second * 5) { 25 | ctx.Logger().Info("sentry flush failed") 26 | } 27 | } 28 | } 29 | 30 | // RecoverWithExit handles panics and reports to Sentry before exiting. 31 | func RecoverWithExit(ctx context.Context) { 32 | if err := recover(); err != nil { 33 | panicStack := string(debug.Stack()) 34 | if eventID := sentry.CurrentHub().Recover(err); eventID != nil { 35 | ctx.Logger().Info("panic captured", "event_id", *eventID) 36 | } 37 | ctx.Logger().Error(fmt.Errorf("panic"), "recovered from panic before exiting", 38 | "stack-trace", panicStack, 39 | "recover", err, 40 | ) 41 | if !sentry.Flush(time.Second * 5) { 42 | ctx.Logger().Info("sentry flush failed") 43 | } 44 | os.Exit(1) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /pkg/config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/trufflesecurity/trufflehog/v3/pkg/custom_detectors" 7 | "github.com/trufflesecurity/trufflehog/v3/pkg/detectors" 8 | "github.com/trufflesecurity/trufflehog/v3/pkg/pb/custom_detectorspb" 9 | "github.com/trufflesecurity/trufflehog/v3/pkg/protoyaml" 10 | ) 11 | 12 | // Config holds user supplied configuration. 13 | type Config struct { 14 | Detectors []detectors.Detector 15 | } 16 | 17 | // Read parses a given filename into a Config. 18 | func Read(filename string) (*Config, error) { 19 | input, err := os.ReadFile(filename) 20 | if err != nil { 21 | return nil, err 22 | } 23 | return NewYAML(input) 24 | } 25 | 26 | // NewYAML parses the given YAML data into a Config. 27 | func NewYAML(input []byte) (*Config, error) { 28 | // Parse the raw YAML into a structure. 29 | var messages custom_detectorspb.CustomDetectors 30 | if err := protoyaml.UnmarshalStrict(input, &messages); err != nil { 31 | return nil, err 32 | } 33 | // Convert the structured YAML into detectors. 34 | var d []detectors.Detector 35 | for _, detectorConfig := range messages.Detectors { 36 | detector, err := custom_detectors.NewWebhookCustomRegex(detectorConfig) 37 | if err != nil { 38 | return nil, err 39 | } 40 | d = append(d, detector) 41 | } 42 | return &Config{ 43 | Detectors: d, 44 | }, nil 45 | } 46 | -------------------------------------------------------------------------------- /pkg/custom_detectors/regex_varstring.go: -------------------------------------------------------------------------------- 1 | package custom_detectors 2 | 3 | import ( 4 | "regexp" 5 | "strconv" 6 | "strings" 7 | ) 8 | 9 | // nameGroupRegex matches `{ name . group }` ignoring any whitespace. 10 | var nameGroupRegex = regexp.MustCompile(`{\s*([a-zA-Z0-9-_]+)\s*(\.\s*[0-9]*)?\s*}`) 11 | 12 | // RegexVarString is a string with embedded {name.group} variables. A name may 13 | // only contain alphanumeric, hyphen, and underscore characters. Group is 14 | // optional but if provided it must be a non-negative integer. If the group is 15 | // omitted it defaults to 0. 16 | type RegexVarString struct { 17 | original string 18 | // map from name to group 19 | variables map[string]int 20 | } 21 | 22 | func NewRegexVarString(original string) RegexVarString { 23 | variables := make(map[string]int) 24 | 25 | matches := nameGroupRegex.FindAllStringSubmatch(original, -1) 26 | for _, match := range matches { 27 | name, group := match[1], 0 28 | // The second match will start with a period followed by any number 29 | // of whitespace. 30 | if len(match[2]) > 1 { 31 | g, err := strconv.Atoi(strings.TrimSpace(match[2][1:])) 32 | if err != nil { 33 | continue 34 | } 35 | group = g 36 | } 37 | variables[name] = group 38 | } 39 | 40 | return RegexVarString{ 41 | original: original, 42 | variables: variables, 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /pkg/custom_detectors/regex_varstring_test.go: -------------------------------------------------------------------------------- 1 | package custom_detectors 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestVarString(t *testing.T) { 10 | tests := []struct { 11 | name string 12 | input string 13 | wantVars map[string]int 14 | }{ 15 | { 16 | name: "empty", 17 | input: "{}", 18 | wantVars: map[string]int{}, 19 | }, 20 | { 21 | name: "no subgroup", 22 | input: "{hello}", 23 | wantVars: map[string]int{ 24 | "hello": 0, 25 | }, 26 | }, 27 | { 28 | name: "with subgroup", 29 | input: "{hello.123}", 30 | wantVars: map[string]int{ 31 | "hello": 123, 32 | }, 33 | }, 34 | { 35 | name: "subgroup with spaces", 36 | input: "{\thell0 . 123 }", 37 | wantVars: map[string]int{ 38 | "hell0": 123, 39 | }, 40 | }, 41 | { 42 | name: "multiple groups", 43 | input: "foo {bar} {bazz.buzz} {buzz.2}", 44 | wantVars: map[string]int{ 45 | "bar": 0, 46 | "buzz": 2, 47 | }, 48 | }, 49 | { 50 | name: "nested groups", 51 | input: "{foo {bar}}", 52 | wantVars: map[string]int{ 53 | "bar": 0, 54 | }, 55 | }, 56 | { 57 | name: "decimal without number", 58 | input: "{foo.}", 59 | wantVars: map[string]int{ 60 | "foo": 0, 61 | }, 62 | }, 63 | { 64 | name: "negative number", 65 | input: "{foo.-1}", 66 | wantVars: map[string]int{}, 67 | }, 68 | } 69 | for _, tt := range tests { 70 | t.Run(tt.name, func(t *testing.T) { 71 | got := NewRegexVarString(tt.input) 72 | assert.Equal(t, tt.input, got.original) 73 | assert.Equal(t, tt.wantVars, got.variables) 74 | }) 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /pkg/decoders/decoders.go: -------------------------------------------------------------------------------- 1 | package decoders 2 | 3 | import ( 4 | "github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb" 5 | "github.com/trufflesecurity/trufflehog/v3/pkg/sources" 6 | ) 7 | 8 | func DefaultDecoders() []Decoder { 9 | return []Decoder{ 10 | // UTF8 must be first for duplicate detection 11 | &UTF8{}, 12 | &Base64{}, 13 | &UTF16{}, 14 | &EscapedUnicode{}, 15 | } 16 | } 17 | 18 | // DecodableChunk is a chunk that includes the type of decoder used. 19 | // This allows us to avoid a type assertion on each decoder. 20 | type DecodableChunk struct { 21 | *sources.Chunk 22 | DecoderType detectorspb.DecoderType 23 | } 24 | 25 | type Decoder interface { 26 | FromChunk(chunk *sources.Chunk) *DecodableChunk 27 | Type() detectorspb.DecoderType 28 | } 29 | 30 | // Fuzz is an entrypoint for go-fuzz, which is an AFL-style fuzzing tool. 31 | // This one attempts to uncover any panics during decoding. 32 | func Fuzz(data []byte) int { 33 | decoded := false 34 | for i, decoder := range DefaultDecoders() { 35 | // Skip the first decoder (plain), because it will always decode and give 36 | // priority to the input (return 1). 37 | if i == 0 { 38 | continue 39 | } 40 | chunk := decoder.FromChunk(&sources.Chunk{Data: data}) 41 | if chunk != nil { 42 | decoded = true 43 | } 44 | } 45 | if decoded { 46 | return 1 // prioritize the input 47 | } 48 | return -1 // Don't add input to the corpus. 49 | } 50 | -------------------------------------------------------------------------------- /pkg/decoders/utf16.go: -------------------------------------------------------------------------------- 1 | package decoders 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | "unicode/utf8" 7 | 8 | "github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb" 9 | "github.com/trufflesecurity/trufflehog/v3/pkg/sources" 10 | ) 11 | 12 | type UTF16 struct{} 13 | 14 | func (d *UTF16) Type() detectorspb.DecoderType { 15 | return detectorspb.DecoderType_UTF16 16 | } 17 | 18 | func (d *UTF16) FromChunk(chunk *sources.Chunk) *DecodableChunk { 19 | if chunk == nil || len(chunk.Data) == 0 { 20 | return nil 21 | } 22 | 23 | decodableChunk := &DecodableChunk{Chunk: chunk, DecoderType: d.Type()} 24 | if utf16Data, err := utf16ToUTF8(chunk.Data); err == nil { 25 | if len(utf16Data) == 0 { 26 | return nil 27 | } 28 | chunk.Data = utf16Data 29 | return decodableChunk 30 | } 31 | 32 | return nil 33 | } 34 | 35 | // utf16ToUTF8 converts a byte slice containing UTF-16 encoded data to a UTF-8 encoded byte slice. 36 | func utf16ToUTF8(b []byte) ([]byte, error) { 37 | var bufBE, bufLE bytes.Buffer 38 | for i := 0; i < len(b)-1; i += 2 { 39 | if r := rune(binary.BigEndian.Uint16(b[i:])); b[i] == 0 && utf8.ValidRune(r) { 40 | if isPrintableByte(byte(r)) { 41 | bufBE.WriteRune(r) 42 | } 43 | } 44 | if r := rune(binary.LittleEndian.Uint16(b[i:])); b[i+1] == 0 && utf8.ValidRune(r) { 45 | if isPrintableByte(byte(r)) { 46 | bufLE.WriteRune(r) 47 | } 48 | } 49 | } 50 | 51 | return append(bufLE.Bytes(), bufBE.Bytes()...), nil 52 | } 53 | -------------------------------------------------------------------------------- /pkg/decoders/utf16_test.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trufflesecurity/trufflehog/5fca1636cda0ebc323168d8d46746ce02786280c/pkg/decoders/utf16_test.dll -------------------------------------------------------------------------------- /pkg/detectors/accuweather/v2/accuweather.go: -------------------------------------------------------------------------------- 1 | package accuweather 2 | 3 | import ( 4 | "context" 5 | 6 | regexp "github.com/wasilibs/go-re2" 7 | 8 | "github.com/trufflesecurity/trufflehog/v3/pkg/detectors" 9 | v1 "github.com/trufflesecurity/trufflehog/v3/pkg/detectors/accuweather/v1" 10 | ) 11 | 12 | type Scanner struct { 13 | v1.Scanner 14 | } 15 | 16 | func (s Scanner) Version() int { return 2 } 17 | 18 | var ( 19 | // Ensure the Scanner satisfies the interface at compile time. 20 | _ detectors.Detector = (*Scanner)(nil) 21 | 22 | // Make sure that your group is surrounded in boundary characters such as below to reduce false positives. 23 | keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"accuweather"}) + `\b([a-zA-Z0-9]{32})\b`) 24 | ) 25 | 26 | // FromData will find and optionally verify Accuweather secrets in a given set of bytes. 27 | func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (results []detectors.Result, err error) { 28 | matches := keyPat.FindAllStringSubmatch(string(data), -1) 29 | return s.ProcessMatches(ctx, matches, verify) 30 | } 31 | -------------------------------------------------------------------------------- /pkg/detectors/aws/common.go: -------------------------------------------------------------------------------- 1 | package aws 2 | 3 | import regexp "github.com/wasilibs/go-re2" 4 | 5 | const ( 6 | RequiredIdEntropy = 3.0 7 | RequiredSecretEntropy = 4.25 8 | ) 9 | 10 | var SecretPat = regexp.MustCompile(`(?:[^A-Za-z0-9+/]|\A)([A-Za-z0-9+/]{40})(?:[^A-Za-z0-9+/]|\z)`) 11 | 12 | type IdentityResponse struct { 13 | GetCallerIdentityResponse struct { 14 | GetCallerIdentityResult struct { 15 | Account string `json:"Account"` 16 | Arn string `json:"Arn"` 17 | UserID string `json:"UserId"` 18 | } `json:"GetCallerIdentityResult"` 19 | ResponseMetadata struct { 20 | RequestID string `json:"RequestId"` 21 | } `json:"ResponseMetadata"` 22 | } `json:"GetCallerIdentityResponse"` 23 | } 24 | 25 | type Error struct { 26 | Code string `json:"Code"` 27 | Message string `json:"Message"` 28 | } 29 | 30 | type ErrorResponseBody struct { 31 | Error Error `json:"Error"` 32 | } 33 | -------------------------------------------------------------------------------- /pkg/detectors/elevenlabs/v1/elevenlabs_integration_test.go: -------------------------------------------------------------------------------- 1 | //go:build detectors 2 | // +build detectors 3 | 4 | package elevenlabs 5 | 6 | import ( 7 | "context" 8 | "testing" 9 | 10 | "github.com/trufflesecurity/trufflehog/v3/pkg/detectors" 11 | ) 12 | 13 | func BenchmarkFromData(benchmark *testing.B) { 14 | ctx := context.Background() 15 | s := Scanner{} 16 | for name, data := range detectors.MustGetBenchmarkData() { 17 | benchmark.Run(name, func(b *testing.B) { 18 | b.ResetTimer() 19 | for n := 0; n < b.N; n++ { 20 | _, err := s.FromData(ctx, false, data) 21 | if err != nil { 22 | b.Fatal(err) 23 | } 24 | } 25 | }) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /pkg/detectors/endpoint_customizer.go: -------------------------------------------------------------------------------- 1 | package detectors 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/trufflesecurity/trufflehog/v3/pkg/common" 7 | ) 8 | 9 | // EndpointSetter implements a sensible default for the SetEndpoints function 10 | // of the EndpointCustomizer interface. A detector can embed this struct to 11 | // gain the functionality. 12 | type EndpointSetter struct { 13 | configuredEndpoints []string 14 | cloudEndpoint string 15 | useCloudEndpoint bool 16 | useFoundEndpoints bool 17 | } 18 | 19 | func (e *EndpointSetter) SetConfiguredEndpoints(userConfiguredEndpoints ...string) error { 20 | if len(userConfiguredEndpoints) == 0 { 21 | return fmt.Errorf("at least one endpoint required") 22 | } 23 | deduped := make([]string, 0, len(userConfiguredEndpoints)) 24 | for _, endpoint := range userConfiguredEndpoints { 25 | common.AddStringSliceItem(endpoint, &deduped) 26 | } 27 | e.configuredEndpoints = deduped 28 | return nil 29 | } 30 | 31 | func (e *EndpointSetter) SetCloudEndpoint(url string) { 32 | e.cloudEndpoint = url 33 | } 34 | 35 | func (e *EndpointSetter) UseCloudEndpoint(enabled bool) { 36 | e.useCloudEndpoint = enabled 37 | } 38 | 39 | func (e *EndpointSetter) UseFoundEndpoints(enabled bool) { 40 | e.useFoundEndpoints = enabled 41 | } 42 | 43 | func (e *EndpointSetter) Endpoints(foundEndpoints ...string) []string { 44 | endpoints := e.configuredEndpoints 45 | if e.useCloudEndpoint && e.cloudEndpoint != "" { 46 | endpoints = append(endpoints, e.cloudEndpoint) 47 | } 48 | if e.useFoundEndpoints { 49 | endpoints = append(endpoints, foundEndpoints...) 50 | } 51 | return endpoints 52 | } 53 | -------------------------------------------------------------------------------- /pkg/detectors/endpoint_customizer_test.go: -------------------------------------------------------------------------------- 1 | package detectors 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestEmbeddedEndpointSetter(t *testing.T) { 10 | type Scanner struct{ EndpointSetter } 11 | 12 | var s Scanner 13 | 14 | t.Run("useFoundEndpoints is true", func(t *testing.T) { 15 | s.useFoundEndpoints = true 16 | 17 | // "baz" is passed to Endpoints, should appear in the result 18 | assert.Equal(t, []string{"baz"}, s.Endpoints("baz")) 19 | }) 20 | 21 | t.Run("setting configured endpoints", func(t *testing.T) { 22 | // Setting "foo" and "bar" 23 | assert.NoError(t, s.SetConfiguredEndpoints("foo", "bar")) 24 | 25 | // Returning error because no endpoints are passed 26 | assert.Error(t, s.SetConfiguredEndpoints()) 27 | }) 28 | 29 | // "foo" and "bar" are added as configured endpoint 30 | 31 | t.Run("useFoundEndpoints adds new endpoints", func(t *testing.T) { 32 | // "baz" is added because useFoundEndpoints is true 33 | assert.Equal(t, []string{"foo", "bar", "baz"}, s.Endpoints("baz")) 34 | }) 35 | 36 | t.Run("useCloudEndpoint is true", func(t *testing.T) { 37 | s.useCloudEndpoint = true 38 | s.cloudEndpoint = "test" 39 | 40 | // "test" is added because useCloudEndpoint is true and cloudEndpoint is set 41 | assert.Equal(t, []string{"foo", "bar", "test"}, s.Endpoints()) 42 | }) 43 | 44 | t.Run("disable both foundEndpoints and cloudEndpoint", func(t *testing.T) { 45 | // now disable both useFoundEndpoints and useCloudEndpoint 46 | s.useFoundEndpoints = false 47 | s.useCloudEndpoint = false 48 | 49 | // "test" won't be added 50 | assert.Equal(t, []string{"foo", "bar"}, s.Endpoints("test")) 51 | }) 52 | 53 | t.Run("cloudEndpoint not added when useCloudEndpoint is false", func(t *testing.T) { 54 | s.cloudEndpoint = "new" 55 | 56 | // "new" is not added because useCloudEndpoint is false 57 | assert.Equal(t, []string{"foo", "bar"}, s.Endpoints()) 58 | }) 59 | 60 | } 61 | -------------------------------------------------------------------------------- /pkg/detectors/eraser/eraser_test.go: -------------------------------------------------------------------------------- 1 | package eraser 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/google/go-cmp/cmp" 8 | 9 | "github.com/trufflesecurity/trufflehog/v3/pkg/detectors" 10 | "github.com/trufflesecurity/trufflehog/v3/pkg/engine/ahocorasick" 11 | ) 12 | 13 | func TestEraser_Pattern(t *testing.T) { 14 | d := Scanner{} 15 | ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d}) 16 | tests := []struct { 17 | name string 18 | input string 19 | want []string 20 | }{ 21 | { 22 | name: "typical pattern", 23 | input: "eraser_token = 'KkBmh6TUBIcyFAp20XXa'", 24 | want: []string{"KkBmh6TUBIcyFAp20XXa"}, 25 | }, 26 | } 27 | 28 | for _, test := range tests { 29 | t.Run(test.name, func(t *testing.T) { 30 | matchedDetectors := ahoCorasickCore.FindDetectorMatches([]byte(test.input)) 31 | if len(matchedDetectors) == 0 { 32 | t.Errorf("keywords '%v' not matched by: %s", d.Keywords(), test.input) 33 | return 34 | } 35 | 36 | results, err := d.FromData(context.Background(), false, []byte(test.input)) 37 | if err != nil { 38 | t.Errorf("error = %v", err) 39 | return 40 | } 41 | 42 | if len(results) != len(test.want) { 43 | if len(results) == 0 { 44 | t.Errorf("did not receive result") 45 | } else { 46 | t.Errorf("expected %d results, only received %d", len(test.want), len(results)) 47 | } 48 | return 49 | } 50 | 51 | actual := make(map[string]struct{}, len(results)) 52 | for _, r := range results { 53 | if len(r.RawV2) > 0 { 54 | actual[string(r.RawV2)] = struct{}{} 55 | } else { 56 | actual[string(r.Raw)] = struct{}{} 57 | } 58 | } 59 | expected := make(map[string]struct{}, len(test.want)) 60 | for _, v := range test.want { 61 | expected[v] = struct{}{} 62 | } 63 | 64 | if diff := cmp.Diff(expected, actual); diff != "" { 65 | t.Errorf("%s diff: (-want +got)\n%s", test.name, diff) 66 | } 67 | }) 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /pkg/detectors/fp_uuids.txt: -------------------------------------------------------------------------------- 1 | 00000000-0000-0000-0000-000000000000 2 | 11111111-1111-1111-1111-111111111111 3 | 22222222-2222-2222-2222-222222222222 4 | 33333333-3333-3333-3333-333333333333 5 | 44444444-4444-4444-4444-444444444444 6 | 55555555-5555-5555-5555-555555555555 7 | 66666666-6666-6666-6666-666666666666 8 | 77777777-7777-7777-7777-777777777777 9 | 88888888-8888-8888-8888-888888888888 10 | 99999999-9999-9999-9999-999999999999 11 | 12345678-1234-1234-1234-123456789abc 12 | 23456789-2345-2345-2345-23456789abcd 13 | 34567890-3456-3456-3456-34567890bcde 14 | 45678901-4567-4567-4567-45678901cdef 15 | 56789012-5678-5678-5678-56789012def0 16 | aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa 17 | bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb 18 | cccccccc-cccc-cccc-cccc-cccccccccccc 19 | dddddddd-dddd-dddd-dddd-dddddddddddd 20 | eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee 21 | ffffffff-ffff-ffff-ffff-ffffffffffff 22 | deadbeef-dead-beef-dead-beefdeadbeef 23 | cafebabe-cafe-babe-cafe-babecafebabe 24 | badc0ffee-badc-0ffe-badc-0ffeebadc0f 25 | deadface-dead-face-dead-facedeadface 26 | feedface-feed-face-feed-facefeedface 27 | a1b2c3d4-a1b2-c3d4-a1b2-c3d4a1b2c3d4 28 | 98765432-9876-5432-9876-543298765432 29 | abcdefab-cdef-abcd-efab-cdefabcdefab 30 | a0a0a0a0-a0a0-a0a0-a0a0-a0a0a0a0a0a0 31 | b0b0b0b0-b0b0-b0b0-b0b0-b0b0b0b0b0b0 32 | c0c0c0c0-c0c0-c0c0-c0c0-c0c0c0c0c0c0 33 | d0d0d0d0-d0d0-d0d0-d0d0-d0d0d0d0d0d0 34 | e0e0e0e0-e0e0-e0e0-e0e0-e0e0e0e0e0e0 35 | f0f0f0f0-f0f0-f0f0-f0f0-f0f0f0f0f0f0 36 | xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx 37 | -xxxx-xxxx-xxxx-xxxxxxxxxxxx 38 | -------------------------------------------------------------------------------- /pkg/detectors/multi_part_credential_provider.go: -------------------------------------------------------------------------------- 1 | package detectors 2 | 3 | var _ MultiPartCredentialProvider = (*DefaultMultiPartCredentialProvider)(nil) 4 | var _ MultiPartCredentialProvider = (*CustomMultiPartCredentialProvider)(nil) 5 | 6 | type DefaultMultiPartCredentialProvider struct{} 7 | 8 | const defaultMaxCredentialSpan = 1024 9 | 10 | // MaxCredentialSpan returns the default maximum credential span of 1024 for the 11 | // DefaultMultiPartCredentialProvider. 12 | func (d DefaultMultiPartCredentialProvider) MaxCredentialSpan() int64 { 13 | return defaultMaxCredentialSpan 14 | } 15 | 16 | type CustomMultiPartCredentialProvider struct{ maxCredentialSpan int64 } 17 | 18 | // NewCustomMultiPartCredentialProvider creates a new instance of CustomMultiPartCredentialProvider 19 | // with the specified maximum credential span. 20 | func NewCustomMultiPartCredentialProvider(maxCredentialSpan int64) *CustomMultiPartCredentialProvider { 21 | return &CustomMultiPartCredentialProvider{maxCredentialSpan: maxCredentialSpan} 22 | } 23 | 24 | // MaxCredentialSpan returns the custom maximum credential span specified during the 25 | // creation of the CustomMultiPartCredentialProvider. 26 | func (d CustomMultiPartCredentialProvider) MaxCredentialSpan() int64 { 27 | return d.maxCredentialSpan 28 | } 29 | -------------------------------------------------------------------------------- /pkg/detectors/multi_part_credential_provider_test.go: -------------------------------------------------------------------------------- 1 | package detectors 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestMultiPartCredentialProviders(t *testing.T) { 10 | testCases := []struct { 11 | name string 12 | provider MultiPartCredentialProvider 13 | expectedSpan int64 14 | }{ 15 | { 16 | name: "DefaultMultiPartCredentialProvider", 17 | provider: DefaultMultiPartCredentialProvider{}, 18 | expectedSpan: defaultMaxCredentialSpan, 19 | }, 20 | { 21 | name: "CustomMultiPartCredentialProvider", 22 | provider: NewCustomMultiPartCredentialProvider(2048), 23 | expectedSpan: 2048, 24 | }, 25 | } 26 | 27 | for _, tc := range testCases { 28 | t.Run(tc.name, func(t *testing.T) { 29 | t.Parallel() 30 | 31 | span := tc.provider.MaxCredentialSpan() 32 | assert.Equal(t, tc.expectedSpan, span) 33 | }) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /pkg/detectors/privatekey/cracker.go: -------------------------------------------------------------------------------- 1 | package privatekey 2 | 3 | import ( 4 | "bytes" 5 | "crypto/x509" 6 | _ "embed" 7 | "errors" 8 | 9 | "golang.org/x/crypto/ssh" 10 | ) 11 | 12 | //go:embed "list.txt" 13 | var rawCrackList []byte 14 | var passphrases [][]byte 15 | 16 | func init() { 17 | passphrases = bytes.Split(rawCrackList, []byte("\n")) 18 | } 19 | 20 | var ( 21 | ErrUncrackable = errors.New("unable to crack encryption") 22 | ) 23 | 24 | func Crack(in []byte) (any, string, error) { 25 | for _, passphrase := range passphrases { 26 | parsed, err := ssh.ParseRawPrivateKeyWithPassphrase(in, passphrase) 27 | if err != nil { 28 | if errors.Is(err, x509.IncorrectPasswordError) { 29 | continue 30 | } else { 31 | return nil, "", err 32 | } 33 | } 34 | return parsed, string(passphrase), nil 35 | } 36 | return nil, "", ErrUncrackable 37 | } 38 | -------------------------------------------------------------------------------- /pkg/detectors/privatekey/fingerprint.go: -------------------------------------------------------------------------------- 1 | package privatekey 2 | 3 | import ( 4 | "crypto/ecdsa" 5 | "crypto/ed25519" 6 | "crypto/rsa" 7 | "crypto/sha1" 8 | "crypto/sha256" 9 | "crypto/x509" 10 | "encoding/base64" 11 | "encoding/hex" 12 | "errors" 13 | "fmt" 14 | 15 | "golang.org/x/crypto/ssh" 16 | ) 17 | 18 | var ( 19 | ErrNotSupported = errors.New("key type not supported") 20 | ErrEncryptedKey = errors.New("key is encrypted") 21 | ) 22 | 23 | func FingerprintPEMKey(parsedKey any) (string, error) { 24 | var pubKey any 25 | switch privateKey := parsedKey.(type) { 26 | case *rsa.PrivateKey: 27 | pubKey = &privateKey.PublicKey 28 | case *ecdsa.PrivateKey: 29 | pubKey = &privateKey.PublicKey 30 | case *ed25519.PrivateKey: 31 | pubKey = privateKey.Public() 32 | // No fingerprinting support for DSA 33 | // case *dsa.PrivateKey: 34 | // pubKey = privateKey.PublicKey 35 | default: 36 | return "", ErrNotSupported 37 | } 38 | 39 | return fingerprintPublicKey(pubKey) 40 | } 41 | 42 | func fingerprintPublicKey(pubKey any) (string, error) { 43 | publickeyBytes, err := x509.MarshalPKIXPublicKey(pubKey) 44 | if err != nil { 45 | return "", err 46 | } 47 | 48 | publicKeyFingerprint := sha1.Sum(publickeyBytes) 49 | publicKeyFingerprintInHex := hex.EncodeToString(publicKeyFingerprint[:]) 50 | return publicKeyFingerprintInHex, nil 51 | } 52 | 53 | func fingerprintSSHPublicKey(pubKey ssh.PublicKey) string { 54 | publicKeyFingerprint := sha256.Sum256(pubKey.Marshal()) 55 | return fmt.Sprintf("SHA256:%s", base64.RawStdEncoding.EncodeToString(publicKeyFingerprint[:])) 56 | } 57 | -------------------------------------------------------------------------------- /pkg/detectors/privatekey/normalize.go: -------------------------------------------------------------------------------- 1 | package privatekey 2 | 3 | import ( 4 | "strings" 5 | ) 6 | 7 | func Normalize(in string) string { 8 | in = strings.ReplaceAll(in, `"`, "") 9 | in = strings.ReplaceAll(in, `'`, "") 10 | in = strings.ReplaceAll(in, "\t", "") 11 | in = strings.ReplaceAll(in, `\t`, "") 12 | in = strings.ReplaceAll(in, `\\t`, "") 13 | in = strings.ReplaceAll(in, `\n`, "\n") 14 | in = strings.ReplaceAll(in, `\\r\\n`, "\n") 15 | in = strings.ReplaceAll(in, `\r\n`, "\n") 16 | in = strings.ReplaceAll(in, "\r\n", "\n") 17 | in = strings.ReplaceAll(in, `\\r`, "\n") 18 | in = strings.ReplaceAll(in, "\r", "\n") 19 | in = strings.ReplaceAll(in, `\r`, "\n") 20 | in = strings.ReplaceAll(in, `\\n`, "\n") 21 | in = strings.ReplaceAll(in, `\n\n`, "\n") 22 | in = strings.ReplaceAll(in, "\n\n", "\n") 23 | in = strings.ReplaceAll(in, `\\`, "\n") 24 | 25 | cleaned := strings.Builder{} 26 | parts := strings.Split(in, "\n") 27 | for _, line := range parts { 28 | cleaned.WriteString(strings.TrimSpace(line) + "\n") 29 | } 30 | return cleaned.String() 31 | } 32 | -------------------------------------------------------------------------------- /pkg/detectors/privatekey/ssh_integration_test.go: -------------------------------------------------------------------------------- 1 | //go:build detectors 2 | // +build detectors 3 | 4 | package privatekey 5 | 6 | import ( 7 | "context" 8 | "testing" 9 | "time" 10 | 11 | "golang.org/x/crypto/ssh" 12 | 13 | "github.com/trufflesecurity/trufflehog/v3/pkg/common" 14 | ) 15 | 16 | func TestFirstResponseFromSSH(t *testing.T) { 17 | ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) 18 | defer cancel() 19 | testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors4") 20 | if err != nil { 21 | t.Fatalf("could not get test secrets from GCP: %s", err) 22 | } 23 | secretGitHub := testSecrets.MustGetField("PRIVATEKEY_GITHUB") 24 | 25 | parsedKey, err := ssh.ParseRawPrivateKey([]byte(Normalize(secretGitHub))) 26 | if err != nil { 27 | t.Fatalf("could not parse test secret: %s", err) 28 | } 29 | 30 | output, err := firstResponseFromSSH(ctx, parsedKey, "git", "github.com:22") 31 | if err != nil { 32 | t.Fail() 33 | } 34 | 35 | if len(output) == 0 { 36 | t.Fail() 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /pkg/detectors/sqlserver/sqlserver_test.go: -------------------------------------------------------------------------------- 1 | package sqlserver 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestSQLServer_Pattern(t *testing.T) { 8 | if !pattern.Match([]byte(`builder.Services.AddDbContext(optionsBuilder => optionsBuilder.UseSqlServer("Server=localhost;Initial Catalog=master;User ID=sa;Password=P@ssw0rd!;Persist Security Info=true;MultipleActiveResultSets=true;"));`)) { 9 | t.Errorf("SQLServer.pattern: did not find connection string from Program.cs") 10 | } 11 | if !pattern.Match([]byte(`{"ConnectionStrings": {"Demo": "Server=localhost;Initial Catalog=master;User ID=sa;Password=P@ssw0rd!;Persist Security Info=true;MultipleActiveResultSets=true;"}}`)) { 12 | t.Errorf("SQLServer.pattern: did not find connection string from appsettings.json") 13 | } 14 | if !pattern.Match([]byte(`CONNECTION_STRING: Server=localhost;Initial Catalog=master;User ID=sa;Password=P@ssw0rd!;Persist Security Info=true;MultipleActiveResultSets=true`)) { 15 | t.Errorf("SQLServer.pattern: did not find connection string from .env") 16 | } 17 | if !pattern.Match([]byte(``)) { 18 | t.Errorf("SQLServer.pattern: did not find connection string in xml format") 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /pkg/engine/circleci.go: -------------------------------------------------------------------------------- 1 | package engine 2 | 3 | import ( 4 | "runtime" 5 | 6 | "google.golang.org/protobuf/proto" 7 | "google.golang.org/protobuf/types/known/anypb" 8 | 9 | "github.com/trufflesecurity/trufflehog/v3/pkg/context" 10 | "github.com/trufflesecurity/trufflehog/v3/pkg/pb/sourcespb" 11 | "github.com/trufflesecurity/trufflehog/v3/pkg/sources" 12 | "github.com/trufflesecurity/trufflehog/v3/pkg/sources/circleci" 13 | ) 14 | 15 | // ScanCircleCI scans CircleCI logs. 16 | func (e *Engine) ScanCircleCI(ctx context.Context, token string) (sources.JobProgressRef, error) { 17 | connection := &sourcespb.CircleCI{ 18 | Credential: &sourcespb.CircleCI_Token{ 19 | Token: token, 20 | }, 21 | } 22 | 23 | var conn anypb.Any 24 | err := anypb.MarshalFrom(&conn, connection, proto.MarshalOptions{}) 25 | if err != nil { 26 | ctx.Logger().Error(err, "failed to marshal Circle CI connection") 27 | return sources.JobProgressRef{}, err 28 | } 29 | 30 | sourceName := "trufflehog - Circle CI" 31 | sourceID, jobID, _ := e.sourceManager.GetIDs(ctx, sourceName, circleci.SourceType) 32 | 33 | circleSource := &circleci.Source{} 34 | if err := circleSource.Init(ctx, "trufflehog - Circle CI", jobID, sourceID, true, &conn, runtime.NumCPU()); err != nil { 35 | return sources.JobProgressRef{}, err 36 | } 37 | return e.sourceManager.EnumerateAndScan(ctx, sourceName, circleSource) 38 | } 39 | -------------------------------------------------------------------------------- /pkg/engine/docker.go: -------------------------------------------------------------------------------- 1 | package engine 2 | 3 | import ( 4 | "runtime" 5 | 6 | "google.golang.org/protobuf/proto" 7 | "google.golang.org/protobuf/types/known/anypb" 8 | 9 | "github.com/trufflesecurity/trufflehog/v3/pkg/context" 10 | "github.com/trufflesecurity/trufflehog/v3/pkg/pb/sourcespb" 11 | "github.com/trufflesecurity/trufflehog/v3/pkg/sources" 12 | "github.com/trufflesecurity/trufflehog/v3/pkg/sources/docker" 13 | ) 14 | 15 | // ScanDocker scans a given docker connection. 16 | func (e *Engine) ScanDocker(ctx context.Context, c sources.DockerConfig) (sources.JobProgressRef, error) { 17 | connection := &sourcespb.Docker{Images: c.Images} 18 | 19 | switch { 20 | case c.UseDockerKeychain: 21 | connection.Credential = &sourcespb.Docker_DockerKeychain{DockerKeychain: true} 22 | case len(c.BearerToken) > 0: 23 | connection.Credential = &sourcespb.Docker_BearerToken{BearerToken: c.BearerToken} 24 | default: 25 | connection.Credential = &sourcespb.Docker_Unauthenticated{} 26 | } 27 | 28 | var conn anypb.Any 29 | err := anypb.MarshalFrom(&conn, connection, proto.MarshalOptions{}) 30 | if err != nil { 31 | ctx.Logger().Error(err, "failed to marshal gitlab connection") 32 | return sources.JobProgressRef{}, err 33 | } 34 | 35 | sourceName := "trufflehog - docker" 36 | sourceID, jobID, _ := e.sourceManager.GetIDs(ctx, sourceName, docker.SourceType) 37 | 38 | dockerSource := &docker.Source{} 39 | if err := dockerSource.Init(ctx, sourceName, jobID, sourceID, true, &conn, runtime.NumCPU()); err != nil { 40 | return sources.JobProgressRef{}, err 41 | } 42 | return e.sourceManager.EnumerateAndScan(ctx, sourceName, dockerSource) 43 | } 44 | -------------------------------------------------------------------------------- /pkg/engine/elasticsearch.go: -------------------------------------------------------------------------------- 1 | package engine 2 | 3 | import ( 4 | "runtime" 5 | 6 | "google.golang.org/protobuf/proto" 7 | "google.golang.org/protobuf/types/known/anypb" 8 | 9 | "github.com/trufflesecurity/trufflehog/v3/pkg/context" 10 | "github.com/trufflesecurity/trufflehog/v3/pkg/pb/sourcespb" 11 | "github.com/trufflesecurity/trufflehog/v3/pkg/sources" 12 | "github.com/trufflesecurity/trufflehog/v3/pkg/sources/elasticsearch" 13 | ) 14 | 15 | // ScanElasticsearch scans a Elasticsearch installation. 16 | func (e *Engine) ScanElasticsearch(ctx context.Context, c sources.ElasticsearchConfig) (sources.JobProgressRef, error) { 17 | connection := &sourcespb.Elasticsearch{ 18 | Nodes: c.Nodes, 19 | Username: c.Username, 20 | Password: c.Password, 21 | CloudId: c.CloudID, 22 | ApiKey: c.APIKey, 23 | ServiceToken: c.ServiceToken, 24 | IndexPattern: c.IndexPattern, 25 | QueryJson: c.QueryJSON, 26 | SinceTimestamp: c.SinceTimestamp, 27 | BestEffortScan: c.BestEffortScan, 28 | } 29 | 30 | var conn anypb.Any 31 | err := anypb.MarshalFrom(&conn, connection, proto.MarshalOptions{}) 32 | if err != nil { 33 | ctx.Logger().Error(err, "failed to marshal Elasticsearch connection") 34 | return sources.JobProgressRef{}, err 35 | } 36 | 37 | sourceName := "trufflehog - Elasticsearch" 38 | sourceID, jobID, _ := e.sourceManager.GetIDs(ctx, sourceName, elasticsearch.SourceType) 39 | 40 | elasticsearchSource := &elasticsearch.Source{} 41 | if err := elasticsearchSource.Init(ctx, sourceName, jobID, sourceID, true, &conn, runtime.NumCPU()); err != nil { 42 | return sources.JobProgressRef{}, err 43 | } 44 | return e.sourceManager.EnumerateAndScan(ctx, sourceName, elasticsearchSource) 45 | } 46 | -------------------------------------------------------------------------------- /pkg/engine/filesystem.go: -------------------------------------------------------------------------------- 1 | package engine 2 | 3 | import ( 4 | "runtime" 5 | 6 | "google.golang.org/protobuf/proto" 7 | "google.golang.org/protobuf/types/known/anypb" 8 | 9 | "github.com/trufflesecurity/trufflehog/v3/pkg/context" 10 | "github.com/trufflesecurity/trufflehog/v3/pkg/pb/sourcespb" 11 | "github.com/trufflesecurity/trufflehog/v3/pkg/sources" 12 | "github.com/trufflesecurity/trufflehog/v3/pkg/sources/filesystem" 13 | ) 14 | 15 | // ScanFileSystem scans a given file system. 16 | func (e *Engine) ScanFileSystem(ctx context.Context, c sources.FilesystemConfig) (sources.JobProgressRef, error) { 17 | connection := &sourcespb.Filesystem{ 18 | Paths: c.Paths, 19 | IncludePathsFile: c.IncludePathsFile, 20 | ExcludePathsFile: c.ExcludePathsFile, 21 | } 22 | var conn anypb.Any 23 | err := anypb.MarshalFrom(&conn, connection, proto.MarshalOptions{}) 24 | if err != nil { 25 | ctx.Logger().Error(err, "failed to marshal filesystem connection") 26 | return sources.JobProgressRef{}, err 27 | } 28 | 29 | sourceName := "trufflehog - filesystem" 30 | sourceID, jobID, _ := e.sourceManager.GetIDs(ctx, sourceName, filesystem.SourceType) 31 | 32 | fileSystemSource := &filesystem.Source{} 33 | if err := fileSystemSource.Init(ctx, sourceName, jobID, sourceID, true, &conn, runtime.NumCPU()); err != nil { 34 | return sources.JobProgressRef{}, err 35 | } 36 | return e.sourceManager.EnumerateAndScan(ctx, sourceName, fileSystemSource) 37 | } 38 | -------------------------------------------------------------------------------- /pkg/engine/git.go: -------------------------------------------------------------------------------- 1 | package engine 2 | 3 | import ( 4 | "runtime" 5 | 6 | "google.golang.org/protobuf/proto" 7 | "google.golang.org/protobuf/types/known/anypb" 8 | 9 | "github.com/trufflesecurity/trufflehog/v3/pkg/context" 10 | "github.com/trufflesecurity/trufflehog/v3/pkg/pb/sourcespb" 11 | "github.com/trufflesecurity/trufflehog/v3/pkg/sources" 12 | "github.com/trufflesecurity/trufflehog/v3/pkg/sources/git" 13 | ) 14 | 15 | // ScanGit scans any git source. 16 | func (e *Engine) ScanGit(ctx context.Context, c sources.GitConfig) (sources.JobProgressRef, error) { 17 | connection := &sourcespb.Git{ 18 | Head: c.HeadRef, 19 | Base: c.BaseRef, 20 | Bare: c.Bare, 21 | Uri: c.URI, 22 | ExcludeGlobs: c.ExcludeGlobs, 23 | IncludePathsFile: c.IncludePathsFile, 24 | ExcludePathsFile: c.ExcludePathsFile, 25 | MaxDepth: int64(c.MaxDepth), 26 | SkipBinaries: c.SkipBinaries, 27 | } 28 | var conn anypb.Any 29 | if err := anypb.MarshalFrom(&conn, connection, proto.MarshalOptions{}); err != nil { 30 | ctx.Logger().Error(err, "failed to marshal git connection") 31 | return sources.JobProgressRef{}, err 32 | } 33 | 34 | sourceName := "trufflehog - git" 35 | sourceID, jobID, _ := e.sourceManager.GetIDs(ctx, sourceName, git.SourceType) 36 | 37 | gitSource := &git.Source{} 38 | if err := gitSource.Init(ctx, sourceName, jobID, sourceID, true, &conn, runtime.NumCPU()); err != nil { 39 | return sources.JobProgressRef{}, err 40 | } 41 | 42 | return e.sourceManager.EnumerateAndScan(ctx, sourceName, gitSource) 43 | } 44 | -------------------------------------------------------------------------------- /pkg/engine/gitlab_integration_test.go: -------------------------------------------------------------------------------- 1 | //go:build integration 2 | // +build integration 3 | 4 | package engine 5 | 6 | import ( 7 | "fmt" 8 | "testing" 9 | 10 | "github.com/stretchr/testify/assert" 11 | "github.com/trufflesecurity/trufflehog/v3/pkg/common" 12 | "github.com/trufflesecurity/trufflehog/v3/pkg/context" 13 | "github.com/trufflesecurity/trufflehog/v3/pkg/sources" 14 | ) 15 | 16 | func TestGitLab(t *testing.T) { 17 | // Run the scan. 18 | ctx := context.Background() 19 | e, err := NewEngine(ctx, &Config{ 20 | Detectors: DefaultDetectors(), 21 | SourceManager: sources.NewManager(), 22 | Verify: false, 23 | }) 24 | assert.NoError(t, err) 25 | e.Start(ctx) 26 | 27 | secret, err := common.GetTestSecret(ctx) 28 | if err != nil { 29 | t.Fatal(fmt.Errorf("failed to access secret: %v", err)) 30 | } 31 | _, err = e.ScanGitLab(ctx, sources.GitlabConfig{ 32 | Token: secret.MustGetField("GITLAB_TOKEN"), 33 | }) 34 | assert.NoError(t, err) 35 | 36 | err = e.Finish(ctx) 37 | assert.NoError(t, err) 38 | 39 | // Check the output provided by metrics. 40 | metrics := e.GetMetrics() 41 | assert.GreaterOrEqual(t, metrics.ChunksScanned, uint64(36312)) 42 | assert.GreaterOrEqual(t, metrics.BytesScanned, uint64(91618854)) 43 | } 44 | -------------------------------------------------------------------------------- /pkg/engine/stdin.go: -------------------------------------------------------------------------------- 1 | package engine 2 | 3 | import ( 4 | "runtime" 5 | 6 | "google.golang.org/protobuf/proto" 7 | "google.golang.org/protobuf/types/known/anypb" 8 | 9 | "github.com/trufflesecurity/trufflehog/v3/pkg/context" 10 | "github.com/trufflesecurity/trufflehog/v3/pkg/pb/sourcespb" 11 | "github.com/trufflesecurity/trufflehog/v3/pkg/sources" 12 | "github.com/trufflesecurity/trufflehog/v3/pkg/sources/stdin" 13 | ) 14 | 15 | // ScanStdinInput scans input that is piped into the application 16 | func (e *Engine) ScanStdinInput(ctx context.Context, c sources.StdinConfig) (sources.JobProgressRef, error) { 17 | connection := &sourcespb.Stdin{} 18 | var conn anypb.Any 19 | err := anypb.MarshalFrom(&conn, connection, proto.MarshalOptions{}) 20 | if err != nil { 21 | ctx.Logger().Error(err, "failed to marshal stdin connection") 22 | return sources.JobProgressRef{}, err 23 | } 24 | 25 | sourceName := "trufflehog - stdin" 26 | sourceID, jobID, _ := e.sourceManager.GetIDs(ctx, sourceName, stdin.SourceType) 27 | 28 | stdinSource := &stdin.Source{} 29 | if err := stdinSource.Init(ctx, sourceName, jobID, sourceID, true, &conn, runtime.NumCPU()); err != nil { 30 | return sources.JobProgressRef{}, err 31 | } 32 | return e.sourceManager.EnumerateAndScan(ctx, sourceName, stdinSource) 33 | } 34 | -------------------------------------------------------------------------------- /pkg/engine/syslog.go: -------------------------------------------------------------------------------- 1 | package engine 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/go-errors/errors" 7 | "google.golang.org/protobuf/proto" 8 | "google.golang.org/protobuf/types/known/anypb" 9 | 10 | "github.com/trufflesecurity/trufflehog/v3/pkg/context" 11 | "github.com/trufflesecurity/trufflehog/v3/pkg/pb/sourcespb" 12 | "github.com/trufflesecurity/trufflehog/v3/pkg/sources" 13 | "github.com/trufflesecurity/trufflehog/v3/pkg/sources/syslog" 14 | ) 15 | 16 | // ScanSyslog is a source that scans syslog files. 17 | func (e *Engine) ScanSyslog(ctx context.Context, c sources.SyslogConfig) (sources.JobProgressRef, error) { 18 | connection := &sourcespb.Syslog{ 19 | Protocol: c.Protocol, 20 | ListenAddress: c.Address, 21 | Format: c.Format, 22 | } 23 | 24 | if c.CertPath != "" && c.KeyPath != "" { 25 | cert, err := os.ReadFile(c.CertPath) 26 | if err != nil { 27 | return sources.JobProgressRef{}, errors.WrapPrefix(err, "could not open TLS cert file", 0) 28 | } 29 | connection.TlsCert = string(cert) 30 | 31 | key, err := os.ReadFile(c.KeyPath) 32 | if err != nil { 33 | return sources.JobProgressRef{}, errors.WrapPrefix(err, "could not open TLS key file", 0) 34 | } 35 | connection.TlsKey = string(key) 36 | } 37 | 38 | var conn anypb.Any 39 | err := anypb.MarshalFrom(&conn, connection, proto.MarshalOptions{}) 40 | if err != nil { 41 | return sources.JobProgressRef{}, errors.WrapPrefix(err, "error unmarshalling connection", 0) 42 | } 43 | 44 | sourceName := "trufflehog - syslog" 45 | sourceID, jobID, _ := e.sourceManager.GetIDs(ctx, sourceName, syslog.SourceType) 46 | syslogSource := &syslog.Source{} 47 | if err := syslogSource.Init(ctx, sourceName, jobID, sourceID, true, &conn, c.Concurrency); err != nil { 48 | return sources.JobProgressRef{}, err 49 | } 50 | syslogSource.InjectConnection(connection) 51 | 52 | return e.sourceManager.EnumerateAndScan(ctx, sourceName, syslogSource) 53 | } 54 | -------------------------------------------------------------------------------- /pkg/engine/testdata/secrets.txt: -------------------------------------------------------------------------------- 1 | 1 aws AKIAWARWQKZNHMZBLY4I 2 | 2 secret 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 3 | sentry 27ac84f4bcdb4fca9701f4d6f6f58cd7d96b69c9d9754d40800645a51d668f90 4 | sentry 27ac84f4bcdb4fca9701f4d6f6f58cd7d96b69c9d9754d40800645a51d668f90 5 | sentry 27ac84f4bcdb4fca9701f4d6f6f58cd7d96b69c9d9754d40800645a51d668f90 6 | sentry 27ac84f4bcdb4fca9701f4d6f6f58cd7d96b69c9d9754d40800645a51d668f90 -------------------------------------------------------------------------------- /pkg/engine/testdata/verificationoverlap_detectors.yaml: -------------------------------------------------------------------------------- 1 | # config.yaml 2 | detectors: 3 | - name: detector1 4 | keywords: 5 | - PMAK 6 | regex: 7 | api_key: \b(PMAK-[a-zA-Z-0-9]{59})\b 8 | 9 | - name: detector2 10 | keywords: 11 | - ost 12 | regex: 13 | api_key: \b([a-zA-Z-0-9]{59})\b -------------------------------------------------------------------------------- /pkg/engine/testdata/verificationoverlap_detectors_fp.yaml: -------------------------------------------------------------------------------- 1 | # config.yaml 2 | detectors: 3 | - name: detector1 4 | keywords: 5 | - sample 6 | regex: 7 | api_key: \b(sample-[a-zA-Z-0-9]{59})\b 8 | 9 | - name: detector2 10 | keywords: 11 | - ample 12 | regex: 13 | api_key: \b(ssample-[a-zA-Z-0-9]{59})\b -------------------------------------------------------------------------------- /pkg/engine/testdata/verificationoverlap_secrets.txt: -------------------------------------------------------------------------------- 1 | 2 | POSTMAN_API_KEY="PMAK-qnwfsLyRSyfCwfpHaQP1UzDhrgpWvHjbYzjpRCMshjt417zWcrzyHUArs7r" 3 | -------------------------------------------------------------------------------- /pkg/engine/testdata/verificationoverlap_secrets_fp.txt: -------------------------------------------------------------------------------- 1 | 2 | POSTMAN_API_KEY="ssample-qnwfsLyRSyfCwfpHaQP1UzDhrgpWvHjbYzjpRCMshjt417zWcrzyHUArs7r" 3 | -------------------------------------------------------------------------------- /pkg/engine/travisci.go: -------------------------------------------------------------------------------- 1 | package engine 2 | 3 | import ( 4 | "runtime" 5 | 6 | "google.golang.org/protobuf/proto" 7 | "google.golang.org/protobuf/types/known/anypb" 8 | 9 | "github.com/trufflesecurity/trufflehog/v3/pkg/context" 10 | "github.com/trufflesecurity/trufflehog/v3/pkg/pb/sourcespb" 11 | "github.com/trufflesecurity/trufflehog/v3/pkg/sources" 12 | "github.com/trufflesecurity/trufflehog/v3/pkg/sources/travisci" 13 | ) 14 | 15 | // ScanTravisCI scans TravisCI logs. 16 | func (e *Engine) ScanTravisCI(ctx context.Context, token string) (sources.JobProgressRef, error) { 17 | connection := &sourcespb.TravisCI{ 18 | Credential: &sourcespb.TravisCI_Token{ 19 | Token: token, 20 | }, 21 | } 22 | 23 | var conn anypb.Any 24 | err := anypb.MarshalFrom(&conn, connection, proto.MarshalOptions{}) 25 | if err != nil { 26 | ctx.Logger().Error(err, "failed to marshal Travis CI connection") 27 | return sources.JobProgressRef{}, err 28 | } 29 | 30 | sourceName := "trufflehog - Travis CI" 31 | sourceID, jobID, _ := e.sourceManager.GetIDs(ctx, sourceName, travisci.SourceType) 32 | 33 | travisSource := &travisci.Source{} 34 | if err := travisSource.Init(ctx, sourceName, jobID, sourceID, true, &conn, runtime.NumCPU()); err != nil { 35 | return sources.JobProgressRef{}, err 36 | } 37 | return e.sourceManager.EnumerateAndScan(ctx, sourceName, travisSource) 38 | } 39 | -------------------------------------------------------------------------------- /pkg/feature/feature.go: -------------------------------------------------------------------------------- 1 | package feature 2 | 3 | import "sync/atomic" 4 | 5 | var ( 6 | ForceSkipBinaries atomic.Bool 7 | ForceSkipArchives atomic.Bool 8 | SkipAdditionalRefs atomic.Bool 9 | EnableAPKHandler atomic.Bool 10 | UserAgentSuffix AtomicString 11 | ) 12 | 13 | type AtomicString struct { 14 | value atomic.Value 15 | } 16 | 17 | // Load returns the current value of the atomic string 18 | func (as *AtomicString) Load() string { 19 | if v := as.value.Load(); v != nil { 20 | return v.(string) 21 | } 22 | return "" 23 | } 24 | 25 | // Store sets the value of the atomic string 26 | func (as *AtomicString) Store(newValue string) { 27 | as.value.Store(newValue) 28 | } 29 | 30 | // Swap atomically swaps the current string with a new one and returns the old value 31 | func (as *AtomicString) Swap(newValue string) string { 32 | oldValue := as.Load() 33 | as.Store(newValue) 34 | return oldValue 35 | } 36 | -------------------------------------------------------------------------------- /pkg/handlers/ar_test.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | "time" 7 | 8 | "github.com/stretchr/testify/assert" 9 | 10 | "github.com/trufflesecurity/trufflehog/v3/pkg/context" 11 | ) 12 | 13 | func TestHandleARFile(t *testing.T) { 14 | file, err := os.Open("testdata/test.deb") 15 | assert.Nil(t, err) 16 | defer file.Close() 17 | 18 | ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) 19 | defer cancel() 20 | 21 | rdr, err := newFileReader(ctx, file) 22 | assert.NoError(t, err) 23 | defer rdr.Close() 24 | 25 | handler := newARHandler() 26 | dataOrErrChan := handler.HandleFile(context.AddLogger(ctx), rdr) 27 | assert.NoError(t, err) 28 | 29 | wantChunkCount := 102 30 | count := 0 31 | for range dataOrErrChan { 32 | count++ 33 | } 34 | 35 | assert.Equal(t, wantChunkCount, count) 36 | } 37 | -------------------------------------------------------------------------------- /pkg/handlers/default_test.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | "time" 7 | 8 | "github.com/stretchr/testify/assert" 9 | 10 | "github.com/trufflesecurity/trufflehog/v3/pkg/context" 11 | ) 12 | 13 | func TestHandleNonArchiveFile(t *testing.T) { 14 | file, err := os.Open("testdata/nonarchive.txt") 15 | assert.Nil(t, err) 16 | defer file.Close() 17 | 18 | ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) 19 | defer cancel() 20 | 21 | rdr, err := newFileReader(ctx, file) 22 | assert.NoError(t, err) 23 | defer rdr.Close() 24 | 25 | handler := newDefaultHandler(defaultHandlerType) 26 | dataOrErrChan := handler.HandleFile(context.AddLogger(ctx), rdr) 27 | assert.NoError(t, err) 28 | 29 | wantChunkCount := 6 30 | count := 0 31 | for range dataOrErrChan { 32 | count++ 33 | } 34 | 35 | assert.Equal(t, wantChunkCount, count) 36 | } 37 | -------------------------------------------------------------------------------- /pkg/handlers/rpm_test.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | "time" 7 | 8 | "github.com/stretchr/testify/assert" 9 | 10 | "github.com/trufflesecurity/trufflehog/v3/pkg/context" 11 | ) 12 | 13 | func TestHandleRPMFile(t *testing.T) { 14 | file, err := os.Open("testdata/test.rpm") 15 | assert.Nil(t, err) 16 | defer file.Close() 17 | 18 | ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) 19 | defer cancel() 20 | 21 | rdr, err := newFileReader(ctx, file) 22 | assert.NoError(t, err) 23 | defer rdr.Close() 24 | 25 | handler := newRPMHandler() 26 | dataOrErrChan := handler.HandleFile(context.AddLogger(ctx), rdr) 27 | assert.NoError(t, err) 28 | 29 | wantChunkCount := 179 30 | count := 0 31 | for range dataOrErrChan { 32 | count++ 33 | } 34 | 35 | assert.Equal(t, wantChunkCount, count) 36 | } 37 | -------------------------------------------------------------------------------- /pkg/handlers/testdata/dir-archive.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trufflesecurity/trufflehog/5fca1636cda0ebc323168d8d46746ce02786280c/pkg/handlers/testdata/dir-archive.zip -------------------------------------------------------------------------------- /pkg/handlers/testdata/example.zip.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trufflesecurity/trufflehog/5fca1636cda0ebc323168d8d46746ce02786280c/pkg/handlers/testdata/example.zip.gz -------------------------------------------------------------------------------- /pkg/handlers/testdata/nested-compressed-archive.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trufflesecurity/trufflehog/5fca1636cda0ebc323168d8d46746ce02786280c/pkg/handlers/testdata/nested-compressed-archive.tar.gz -------------------------------------------------------------------------------- /pkg/handlers/testdata/nested-dirs.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trufflesecurity/trufflehog/5fca1636cda0ebc323168d8d46746ce02786280c/pkg/handlers/testdata/nested-dirs.zip -------------------------------------------------------------------------------- /pkg/handlers/testdata/test.deb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trufflesecurity/trufflehog/5fca1636cda0ebc323168d8d46746ce02786280c/pkg/handlers/testdata/test.deb -------------------------------------------------------------------------------- /pkg/handlers/testdata/test.rpm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trufflesecurity/trufflehog/5fca1636cda0ebc323168d8d46746ce02786280c/pkg/handlers/testdata/test.rpm -------------------------------------------------------------------------------- /pkg/handlers/testdata/test.tgz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trufflesecurity/trufflehog/5fca1636cda0ebc323168d8d46746ce02786280c/pkg/handlers/testdata/test.tgz -------------------------------------------------------------------------------- /pkg/handlers/testdata/testdir.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trufflesecurity/trufflehog/5fca1636cda0ebc323168d8d46746ce02786280c/pkg/handlers/testdata/testdir.zip -------------------------------------------------------------------------------- /pkg/hasher/blake2b.go: -------------------------------------------------------------------------------- 1 | package hasher 2 | 3 | import "golang.org/x/crypto/blake2b" 4 | 5 | // Blake2b implements the Hasher interface using Blake2b algorithm. 6 | type Blake2b struct{ baseHasher } 7 | 8 | // NewBlake2B creates a new Blake2b hasher. 9 | func NewBlake2B() *Blake2b { 10 | h, _ := blake2b.New256(nil) 11 | return &Blake2b{baseHasher: baseHasher{hash: h}} 12 | } 13 | -------------------------------------------------------------------------------- /pkg/log/dynamic_redactor.go: -------------------------------------------------------------------------------- 1 | package log 2 | 3 | import ( 4 | "strings" 5 | "sync" 6 | "sync/atomic" 7 | ) 8 | 9 | type dynamicRedactor struct { 10 | denySet map[string]struct{} 11 | denySlice []string 12 | denyMu sync.Mutex 13 | 14 | replacer atomic.Pointer[strings.Replacer] 15 | } 16 | 17 | var globalRedactor *dynamicRedactor 18 | 19 | func init() { 20 | globalRedactor = &dynamicRedactor{denySet: make(map[string]struct{})} 21 | globalRedactor.replacer.CompareAndSwap(nil, strings.NewReplacer()) 22 | } 23 | 24 | // RedactGlobally configures the global log redactor to redact the provided value during log emission. The value will be 25 | // redacted in log messages and values that are strings, but not in log keys or values of other types. 26 | func RedactGlobally(sensitiveValue string) { 27 | globalRedactor.configureForRedaction(sensitiveValue) 28 | } 29 | 30 | func (r *dynamicRedactor) configureForRedaction(sensitiveValue string) { 31 | if sensitiveValue == "" { 32 | return 33 | } 34 | 35 | r.denyMu.Lock() 36 | defer r.denyMu.Unlock() 37 | 38 | if _, ok := r.denySet[sensitiveValue]; ok { 39 | return 40 | } 41 | 42 | r.denySet[sensitiveValue] = struct{}{} 43 | r.denySlice = append(r.denySlice, sensitiveValue, "*****") 44 | 45 | r.replacer.Store(strings.NewReplacer(r.denySlice...)) 46 | } 47 | 48 | func (r *dynamicRedactor) redact(s string) string { 49 | return r.replacer.Load().Replace(s) 50 | } 51 | -------------------------------------------------------------------------------- /pkg/log/level.go: -------------------------------------------------------------------------------- 1 | package log 2 | 3 | import ( 4 | "go.uber.org/zap" 5 | "go.uber.org/zap/zapcore" 6 | ) 7 | 8 | var ( 9 | // Global, default log level control. 10 | globalLogLevel levelSetter = zap.NewAtomicLevel() 11 | ) 12 | 13 | type levelSetter interface { 14 | zapcore.LevelEnabler 15 | SetLevel(zapcore.Level) 16 | Level() zapcore.Level 17 | } 18 | 19 | // SetLevel sets the log level for loggers created with the default level 20 | // controller. 21 | func SetLevel(level int8) { 22 | SetLevelForControl(globalLogLevel, level) 23 | } 24 | 25 | // SetLevelForControl sets the log level for a given control. 26 | func SetLevelForControl(control levelSetter, level int8) { 27 | // Zap's levels get more verbose as the number gets smaller, as explained 28 | // by zapr here: https://github.com/go-logr/zapr#increasing-verbosity 29 | // For example setting the level to -2 below, means log.V(2) will be enabled. 30 | control.SetLevel(zapcore.Level(-level)) 31 | } 32 | -------------------------------------------------------------------------------- /pkg/log/redaction_core.go: -------------------------------------------------------------------------------- 1 | package log 2 | 3 | import ( 4 | "go.uber.org/zap/zapcore" 5 | ) 6 | 7 | // redactionCore wraps a zapcore.Core to perform redaction of log messages in 8 | // the message and field values. 9 | type redactionCore struct { 10 | zapcore.Core 11 | redactor *dynamicRedactor 12 | } 13 | 14 | // NewRedactionCore creates a zapcore.Core that performs redaction of logs in 15 | // the message and field values. 16 | func NewRedactionCore(core zapcore.Core, redactor *dynamicRedactor) zapcore.Core { 17 | return &redactionCore{core, redactor} 18 | } 19 | 20 | // Check overrides the embedded zapcore.Core Check() method to add the 21 | // redactionCore to the zapcore.CheckedEntry. 22 | func (c *redactionCore) Check(ent zapcore.Entry, ce *zapcore.CheckedEntry) *zapcore.CheckedEntry { 23 | if c.Enabled(ent.Level) { 24 | return ce.AddCore(ent, c) 25 | } 26 | return ce 27 | } 28 | 29 | func (c *redactionCore) With(fields []zapcore.Field) zapcore.Core { 30 | return NewRedactionCore(c.Core.With(fields), c.redactor) 31 | } 32 | 33 | // Write overrides the embedded zapcore.Core Write() method to redact the message and fields before passing them to be 34 | // written. Only message and string values are redacted; keys and non-string values (e.g. those inside of arrays and 35 | // structured objects) are not redacted. 36 | func (c *redactionCore) Write(ent zapcore.Entry, fields []zapcore.Field) error { 37 | ent.Message = c.redactor.redact(ent.Message) 38 | for i := range fields { 39 | fields[i].String = c.redactor.redact(fields[i].String) 40 | } 41 | return c.Core.Write(ent, fields) 42 | } 43 | -------------------------------------------------------------------------------- /pkg/process/zombies.go: -------------------------------------------------------------------------------- 1 | package process 2 | 3 | import ( 4 | "os/exec" 5 | "runtime" 6 | "strings" 7 | ) 8 | 9 | func GetGitProcessList() []string { 10 | var cmd *exec.Cmd 11 | if runtime.GOOS == "darwin" { 12 | cmd = exec.Command("ps", "-eo", "pid,state,command") 13 | } else { 14 | cmd = exec.Command("ps", "-eo", "pid,stat,cmd") 15 | } 16 | 17 | output, err := cmd.Output() 18 | if err != nil { 19 | return nil 20 | } 21 | 22 | lines := strings.Split(string(output), "\n") 23 | var gitProcesses []string 24 | for _, line := range lines { 25 | if strings.Contains(line, "git") { 26 | gitProcesses = append(gitProcesses, line) 27 | } 28 | } 29 | return gitProcesses 30 | } 31 | 32 | func DetectGitZombies(before, after []string) []string { 33 | beforeMap := make(map[string]bool) 34 | for _, process := range before { 35 | beforeMap[process] = true 36 | } 37 | 38 | var zombies []string 39 | for _, process := range after { 40 | if !beforeMap[process] { 41 | fields := strings.Fields(process) 42 | if len(fields) >= 2 && (fields[1] == "Z" || strings.HasPrefix(fields[1], "Z")) { 43 | zombies = append(zombies, process) 44 | } 45 | } 46 | } 47 | return zombies 48 | } 49 | -------------------------------------------------------------------------------- /pkg/protoyaml/protoyaml.go: -------------------------------------------------------------------------------- 1 | package protoyaml 2 | 3 | import ( 4 | "google.golang.org/protobuf/encoding/protojson" 5 | "google.golang.org/protobuf/proto" 6 | "sigs.k8s.io/yaml" 7 | ) 8 | 9 | var nonStrict = protojson.UnmarshalOptions{DiscardUnknown: true} 10 | var strict = protojson.UnmarshalOptions{DiscardUnknown: false} 11 | 12 | // Marshal writes the given proto.Message in YAML format. 13 | func Marshal(m proto.Message) ([]byte, error) { 14 | json, err := protojson.Marshal(m) 15 | if err != nil { 16 | return nil, err 17 | } 18 | return yaml.JSONToYAML(json) 19 | } 20 | 21 | // Unmarshal reads the given []byte into the given proto.Message, discarding 22 | // any unknown fields in the input. 23 | func Unmarshal(b []byte, m proto.Message) error { 24 | json, err := yaml.YAMLToJSON(b) 25 | if err != nil { 26 | return err 27 | } 28 | return nonStrict.Unmarshal(json, m) 29 | } 30 | 31 | // UnmarshalStrict reads the given []byte into the given proto.Message. If there 32 | // are any unknown fields in the input, an error is returned. 33 | func UnmarshalStrict(b []byte, m proto.Message) error { 34 | json, err := yaml.YAMLToJSON(b) 35 | if err != nil { 36 | return err 37 | } 38 | return strict.Unmarshal(json, m) 39 | } 40 | -------------------------------------------------------------------------------- /pkg/sanitizer/utf8.go: -------------------------------------------------------------------------------- 1 | package sanitizer 2 | 3 | import ( 4 | "strings" 5 | ) 6 | 7 | func UTF8(in string) string { 8 | return strings.Replace(strings.ToValidUTF8(in, "❗"), "\x00", "", -1) 9 | } 10 | -------------------------------------------------------------------------------- /pkg/sanitizer/utf8_test.go: -------------------------------------------------------------------------------- 1 | package sanitizer 2 | 3 | import "testing" 4 | 5 | func TestUTF8(t *testing.T) { 6 | type args struct { 7 | in string 8 | } 9 | tests := []struct { 10 | name string 11 | args args 12 | want string 13 | }{ 14 | { 15 | name: "valid", 16 | args: args{ 17 | in: "hello123", 18 | }, 19 | want: "hello123", 20 | }, 21 | { 22 | name: "sanitized", 23 | args: args{ 24 | in: "Gr\351gory Smith", 25 | }, 26 | want: "Gr❗gory Smith", 27 | }, 28 | { 29 | name: "sanitized", 30 | args: args{ 31 | in: "no \x00 nulls because postgres does not support it in text fields", 32 | }, 33 | want: "no nulls because postgres does not support it in text fields", 34 | }, 35 | } 36 | for _, tt := range tests { 37 | t.Run(tt.name, func(t *testing.T) { 38 | if got := UTF8(tt.args.in); got != tt.want { 39 | t.Errorf("UTF8() = %v, want %v", got, tt.want) 40 | } 41 | }) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /pkg/sources/docker/metrics.go: -------------------------------------------------------------------------------- 1 | package docker 2 | 3 | import ( 4 | "github.com/prometheus/client_golang/prometheus" 5 | "github.com/prometheus/client_golang/prometheus/promauto" 6 | 7 | "github.com/trufflesecurity/trufflehog/v3/pkg/common" 8 | ) 9 | 10 | var ( 11 | dockerLayersScanned = promauto.NewGaugeVec(prometheus.GaugeOpts{ 12 | Namespace: common.MetricsNamespace, 13 | Subsystem: common.MetricsSubsystem, 14 | Name: "docker_layers_scanned", 15 | Help: "Total number of Docker layers scanned.", 16 | }, 17 | []string{"source_name"}) 18 | 19 | dockerHistoryEntriesScanned = promauto.NewGaugeVec(prometheus.GaugeOpts{ 20 | Namespace: common.MetricsNamespace, 21 | Subsystem: common.MetricsSubsystem, 22 | Name: "docker_history_entries_scanned", 23 | Help: "Total number of Docker image history entries scanned.", 24 | }, 25 | []string{"source_name"}) 26 | 27 | dockerImagesScanned = promauto.NewGaugeVec(prometheus.GaugeOpts{ 28 | Namespace: common.MetricsNamespace, 29 | Subsystem: common.MetricsSubsystem, 30 | Name: "docker_images_scanned", 31 | Help: "Total number of Docker images scanned.", 32 | }, 33 | []string{"source_name"}) 34 | ) 35 | -------------------------------------------------------------------------------- /pkg/sources/errors.go: -------------------------------------------------------------------------------- 1 | package sources 2 | 3 | import ( 4 | "errors" 5 | "strings" 6 | "sync" 7 | ) 8 | 9 | // ScanErrors is used to collect errors encountered while scanning. 10 | // It ensures that errors are collected in a thread-safe manner. 11 | type ScanErrors struct { 12 | mu sync.RWMutex 13 | errors []error 14 | } 15 | 16 | // NewScanErrors creates a new thread safe error collector. 17 | func NewScanErrors() *ScanErrors { 18 | return &ScanErrors{errors: make([]error, 0)} 19 | } 20 | 21 | // Add an error to the collection in a thread-safe manner. 22 | func (s *ScanErrors) Add(err error) { 23 | if err == nil { 24 | return 25 | } 26 | 27 | s.mu.Lock() 28 | defer s.mu.Unlock() 29 | s.errors = append(s.errors, err) 30 | } 31 | 32 | // Count returns the number of errors collected. 33 | func (s *ScanErrors) Count() uint64 { 34 | s.mu.RLock() 35 | defer s.mu.RUnlock() 36 | return uint64(len(s.errors)) 37 | } 38 | 39 | func (s *ScanErrors) String() string { 40 | s.mu.RLock() 41 | defer s.mu.RUnlock() 42 | 43 | var sb strings.Builder 44 | sb.WriteString("[") 45 | for i, err := range s.errors { 46 | sb.WriteString(`"` + err.Error() + `"`) 47 | if i < len(s.errors)-1 { 48 | sb.WriteString(", ") 49 | } 50 | } 51 | sb.WriteString("]") 52 | return sb.String() 53 | } 54 | 55 | func (s *ScanErrors) Errors() error { 56 | s.mu.RLock() 57 | defer s.mu.RUnlock() 58 | return errors.Join(s.errors...) 59 | } 60 | 61 | // TargetedScanError is an error with a secret ID attached. Collections of them can be returned by targeted scans that 62 | // scan multiple targets in order to associate individual errors with individual scan targets. 63 | type TargetedScanError struct { 64 | Err error 65 | SecretID int64 66 | } 67 | 68 | var _ error = (*TargetedScanError)(nil) 69 | 70 | func (t TargetedScanError) Error() string { 71 | return t.Err.Error() 72 | } 73 | 74 | func (t TargetedScanError) Unwrap() error { 75 | return t.Err 76 | } 77 | -------------------------------------------------------------------------------- /pkg/sources/git/cmd_check.go: -------------------------------------------------------------------------------- 1 | package git 2 | 3 | import ( 4 | "fmt" 5 | "os/exec" 6 | "regexp" 7 | "strconv" 8 | "strings" 9 | 10 | "github.com/go-errors/errors" 11 | ) 12 | 13 | // Extract the version string using a regex to find the version numbers 14 | var regex = regexp.MustCompile(`\d+\.\d+\.\d+`) 15 | 16 | // CmdCheck checks if git is installed and meets 2.20.0<=x<3.0.0 version requirements. 17 | func CmdCheck() error { 18 | if errors.Is(exec.Command("git").Run(), exec.ErrNotFound) { 19 | return fmt.Errorf("'git' command not found in $PATH. Make sure git is installed and included in $PATH") 20 | } 21 | 22 | // Check the version is greater than or equal to 2.20.0 23 | out, err := exec.Command("git", "--version").Output() 24 | if err != nil { 25 | return fmt.Errorf("failed to check git version: %w", err) 26 | } 27 | 28 | versionStr := regex.FindString(string(out)) 29 | versionParts := strings.Split(versionStr, ".") 30 | 31 | // Parse version numbers 32 | major, _ := strconv.Atoi(versionParts[0]) 33 | minor, _ := strconv.Atoi(versionParts[1]) 34 | 35 | // Compare with version 2.20.0<=x<3.0.0 36 | if major == 2 && minor >= 20 { 37 | return nil 38 | } 39 | 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) 40 | } 41 | -------------------------------------------------------------------------------- /pkg/sources/git/scan_options.go: -------------------------------------------------------------------------------- 1 | package git 2 | 3 | import ( 4 | "github.com/go-git/go-git/v5" 5 | "github.com/trufflesecurity/trufflehog/v3/pkg/common" 6 | ) 7 | 8 | type ScanOptions struct { 9 | Filter *common.Filter 10 | BaseHash string // When scanning a git.Log, this is the oldest/first commit. 11 | HeadHash string 12 | MaxDepth int64 13 | Bare bool 14 | ExcludeGlobs []string 15 | LogOptions *git.LogOptions 16 | } 17 | 18 | type ScanOption func(*ScanOptions) 19 | 20 | func ScanOptionFilter(filter *common.Filter) ScanOption { 21 | return func(scanOptions *ScanOptions) { 22 | scanOptions.Filter = filter 23 | } 24 | } 25 | 26 | func ScanOptionBaseHash(hash string) ScanOption { 27 | return func(scanOptions *ScanOptions) { 28 | scanOptions.BaseHash = hash 29 | } 30 | } 31 | 32 | func ScanOptionHeadCommit(hash string) ScanOption { 33 | return func(scanOptions *ScanOptions) { 34 | scanOptions.HeadHash = hash 35 | } 36 | } 37 | 38 | func ScanOptionMaxDepth(maxDepth int64) ScanOption { 39 | return func(scanOptions *ScanOptions) { 40 | scanOptions.MaxDepth = maxDepth 41 | } 42 | } 43 | 44 | func ScanOptionExcludeGlobs(globs []string) ScanOption { 45 | return func(scanOptions *ScanOptions) { 46 | scanOptions.ExcludeGlobs = globs 47 | } 48 | } 49 | 50 | func ScanOptionLogOptions(logOptions *git.LogOptions) ScanOption { 51 | return func(scanOptions *ScanOptions) { 52 | scanOptions.LogOptions = logOptions 53 | } 54 | } 55 | 56 | func ScanOptionBare(bare bool) ScanOption { 57 | return func(scanOptions *ScanOptions) { 58 | scanOptions.Bare = bare 59 | } 60 | } 61 | 62 | func NewScanOptions(options ...ScanOption) *ScanOptions { 63 | scanOptions := &ScanOptions{ 64 | Filter: common.FilterEmpty(), 65 | BaseHash: "", 66 | HeadHash: "", 67 | MaxDepth: -1, 68 | LogOptions: &git.LogOptions{ 69 | All: true, 70 | }, 71 | } 72 | for _, option := range options { 73 | option(scanOptions) 74 | } 75 | return scanOptions 76 | } 77 | -------------------------------------------------------------------------------- /pkg/sources/git/unit.go: -------------------------------------------------------------------------------- 1 | package git 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "net/url" 7 | "path/filepath" 8 | "strings" 9 | 10 | "github.com/trufflesecurity/trufflehog/v3/pkg/sources" 11 | ) 12 | 13 | const ( 14 | UnitRepo sources.SourceUnitKind = "repo" 15 | UnitDir sources.SourceUnitKind = "dir" 16 | ) 17 | 18 | // Ensure SourceUnit implements the interface at compile time. 19 | var _ sources.SourceUnit = SourceUnit{} 20 | 21 | // A git source unit can be two kinds of units: either a local directory path 22 | // or a remote repository. 23 | type SourceUnit struct { 24 | Kind sources.SourceUnitKind `json:"kind"` 25 | ID string `json:"id"` 26 | } 27 | 28 | // Implement sources.SourceUnit interface. 29 | func (u SourceUnit) SourceUnitID() (string, sources.SourceUnitKind) { 30 | return u.ID, u.Kind 31 | } 32 | 33 | // Provide a custom Display method. 34 | func (u SourceUnit) Display() string { 35 | switch u.Kind { 36 | case UnitRepo: 37 | repo := u.ID 38 | if parsedURL, err := url.Parse(u.ID); err == nil { 39 | // scheme://host/owner/repo 40 | repo = strings.TrimPrefix(parsedURL.Path, "/") 41 | } else if _, path, found := strings.Cut(u.ID, ":"); found { 42 | // git@host:owner/repo 43 | // TODO: Is this possible? We should maybe canonicalize 44 | // the URL before getting here. 45 | repo = path 46 | } 47 | return strings.TrimSuffix(repo, ".git") 48 | case UnitDir: 49 | return filepath.Base(u.ID) 50 | default: 51 | return "mysterious git unit" 52 | } 53 | } 54 | 55 | // Helper function to unmarshal raw bytes into our SourceUnit struct. 56 | func UnmarshalUnit(data []byte) (sources.SourceUnit, error) { 57 | var unit SourceUnit 58 | if err := json.Unmarshal(data, &unit); err != nil { 59 | return nil, err 60 | } 61 | if unit.ID == "" || (unit.Kind != UnitRepo && unit.Kind != UnitDir) { 62 | return nil, fmt.Errorf("not a git.SourceUnit") 63 | } 64 | return unit, nil 65 | } 66 | -------------------------------------------------------------------------------- /pkg/sources/git/unit_test.go: -------------------------------------------------------------------------------- 1 | package git 2 | 3 | import ( 4 | "encoding/json" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestUnmarshalUnit(t *testing.T) { 11 | s := `{"kind":"repo","id":"https://github.com/trufflesecurity/test_keys.git"}` 12 | expectedUnit := SourceUnit{ID: "https://github.com/trufflesecurity/test_keys.git", Kind: UnitRepo} 13 | gotUnit, err := UnmarshalUnit([]byte(s)) 14 | assert.NoError(t, err) 15 | 16 | assert.Equal(t, expectedUnit, gotUnit) 17 | 18 | _, err = UnmarshalUnit(nil) 19 | assert.Error(t, err) 20 | 21 | _, err = UnmarshalUnit([]byte(`{"kind":"idk","id":"id"}`)) 22 | assert.Error(t, err) 23 | } 24 | 25 | func TestMarshalUnit(t *testing.T) { 26 | unit := SourceUnit{ID: "https://github.com/trufflesecurity/test_keys.git", Kind: UnitRepo} 27 | b, err := json.Marshal(unit) 28 | assert.NoError(t, err) 29 | 30 | assert.Equal(t, `{"kind":"repo","id":"https://github.com/trufflesecurity/test_keys.git"}`, string(b)) 31 | } 32 | 33 | func TestDisplayUnit(t *testing.T) { 34 | unit := SourceUnit{ID: "https://github.com/trufflesecurity/test_keys.git", Kind: UnitRepo} 35 | assert.Equal(t, "trufflesecurity/test_keys", unit.Display()) 36 | 37 | unit = SourceUnit{ID: "/path/to/repo", Kind: UnitDir} 38 | assert.Equal(t, "repo", unit.Display()) 39 | 40 | unit = SourceUnit{ID: "ssh://github.com/trufflesecurity/test_keys", Kind: UnitRepo} 41 | assert.Equal(t, "trufflesecurity/test_keys", unit.Display()) 42 | } 43 | -------------------------------------------------------------------------------- /pkg/sources/github/connector.go: -------------------------------------------------------------------------------- 1 | package github 2 | 3 | import ( 4 | gogit "github.com/go-git/go-git/v5" 5 | "github.com/google/go-github/v67/github" 6 | "github.com/trufflesecurity/trufflehog/v3/pkg/context" 7 | ) 8 | 9 | const cloudEndpoint = "https://api.github.com" 10 | 11 | // Connector abstracts over the authenticated ways to interact with GitHub: cloning and API operations. 12 | type Connector interface { 13 | // APIClient returns a configured GitHub client that can be used for GitHub API operations. 14 | APIClient() *github.Client 15 | // Clone clones a repository using the configured authentication information. 16 | Clone(ctx context.Context, repoURL string, args ...string) (string, *gogit.Repository, error) 17 | } 18 | -------------------------------------------------------------------------------- /pkg/sources/github/connector_basicauth.go: -------------------------------------------------------------------------------- 1 | package github 2 | 3 | import ( 4 | "fmt" 5 | 6 | gogit "github.com/go-git/go-git/v5" 7 | "github.com/google/go-github/v67/github" 8 | "github.com/trufflesecurity/trufflehog/v3/pkg/common" 9 | "github.com/trufflesecurity/trufflehog/v3/pkg/context" 10 | "github.com/trufflesecurity/trufflehog/v3/pkg/pb/credentialspb" 11 | "github.com/trufflesecurity/trufflehog/v3/pkg/sources/git" 12 | ) 13 | 14 | type basicAuthConnector struct { 15 | apiClient *github.Client 16 | username string 17 | password string 18 | } 19 | 20 | var _ Connector = (*basicAuthConnector)(nil) 21 | 22 | func NewBasicAuthConnector(apiEndpoint string, cred *credentialspb.BasicAuth) (Connector, error) { 23 | const httpTimeoutSeconds = 60 24 | httpClient := common.RetryableHTTPClientTimeout(int64(httpTimeoutSeconds)) 25 | httpClient.Transport = &github.BasicAuthTransport{ 26 | Username: cred.Username, 27 | Password: cred.Password, 28 | } 29 | 30 | apiClient, err := createGitHubClient(httpClient, apiEndpoint) 31 | if err != nil { 32 | return nil, fmt.Errorf("could not create API client: %w", err) 33 | } 34 | 35 | return &basicAuthConnector{ 36 | apiClient: apiClient, 37 | username: cred.Username, 38 | password: cred.Password, 39 | }, nil 40 | } 41 | 42 | func (c *basicAuthConnector) APIClient() *github.Client { 43 | return c.apiClient 44 | } 45 | 46 | func (c *basicAuthConnector) Clone(ctx context.Context, repoURL string, args ...string) (string, *gogit.Repository, error) { 47 | return git.CloneRepoUsingToken(ctx, c.password, repoURL, c.username, true, args...) 48 | } 49 | -------------------------------------------------------------------------------- /pkg/sources/github/connector_unauthenticated.go: -------------------------------------------------------------------------------- 1 | package github 2 | 3 | import ( 4 | "fmt" 5 | 6 | gogit "github.com/go-git/go-git/v5" 7 | "github.com/google/go-github/v67/github" 8 | 9 | "github.com/trufflesecurity/trufflehog/v3/pkg/common" 10 | "github.com/trufflesecurity/trufflehog/v3/pkg/context" 11 | "github.com/trufflesecurity/trufflehog/v3/pkg/sources/git" 12 | ) 13 | 14 | type unauthenticatedConnector struct { 15 | apiClient *github.Client 16 | } 17 | 18 | var _ Connector = (*unauthenticatedConnector)(nil) 19 | 20 | func NewUnauthenticatedConnector(apiEndpoint string) (Connector, error) { 21 | const httpTimeoutSeconds = 60 22 | httpClient := common.RetryableHTTPClientTimeout(int64(httpTimeoutSeconds)) 23 | apiClient, err := createGitHubClient(httpClient, apiEndpoint) 24 | if err != nil { 25 | return nil, fmt.Errorf("could not create API client: %w", err) 26 | } 27 | return &unauthenticatedConnector{ 28 | apiClient: apiClient, 29 | }, nil 30 | } 31 | 32 | func (c *unauthenticatedConnector) APIClient() *github.Client { 33 | return c.apiClient 34 | } 35 | 36 | func (c *unauthenticatedConnector) Clone(ctx context.Context, repoURL string, args ...string) (string, *gogit.Repository, error) { 37 | return git.CloneRepoUsingUnauthenticated(ctx, repoURL, args...) 38 | } 39 | -------------------------------------------------------------------------------- /pkg/sources/github/metrics.go: -------------------------------------------------------------------------------- 1 | package github 2 | 3 | import ( 4 | "github.com/prometheus/client_golang/prometheus" 5 | "github.com/prometheus/client_golang/prometheus/promauto" 6 | 7 | "github.com/trufflesecurity/trufflehog/v3/pkg/common" 8 | ) 9 | 10 | var ( 11 | githubNumRateLimitEncountered = promauto.NewGaugeVec(prometheus.GaugeOpts{ 12 | Namespace: common.MetricsNamespace, 13 | Subsystem: common.MetricsSubsystem, 14 | Name: "github_num_rate_limit_encountered", 15 | Help: "Total number of times Github Rate Limit was encountered", 16 | }, 17 | []string{"source_name"}) 18 | 19 | githubSecondsSpentRateLimited = promauto.NewGaugeVec(prometheus.GaugeOpts{ 20 | Namespace: common.MetricsNamespace, 21 | Subsystem: common.MetricsSubsystem, 22 | Name: "github_seconds_spent_rate_limited", 23 | Help: "Total number of seconds spent idle due to GitHub rate limits.", 24 | }, 25 | []string{"source_name"}) 26 | 27 | githubReposEnumerated = promauto.NewGaugeVec(prometheus.GaugeOpts{ 28 | Namespace: common.MetricsNamespace, 29 | Subsystem: common.MetricsSubsystem, 30 | Name: "github_repos_enumerated", 31 | Help: "Total number of GitHub repositories enumerated.", 32 | }, 33 | []string{"source_name"}) 34 | 35 | githubReposScanned = promauto.NewGaugeVec(prometheus.GaugeOpts{ 36 | Namespace: common.MetricsNamespace, 37 | Subsystem: common.MetricsSubsystem, 38 | Name: "github_repos_scanned", 39 | Help: "Total number of GitHub repositories scanned.", 40 | }, 41 | []string{"source_name"}) 42 | 43 | githubOrgsEnumerated = promauto.NewGaugeVec(prometheus.GaugeOpts{ 44 | Namespace: common.MetricsNamespace, 45 | Subsystem: common.MetricsSubsystem, 46 | Name: "github_orgs_enumerated", 47 | Help: "Total number of GitHub organizations enumerated.", 48 | }, 49 | []string{"source_name"}) 50 | ) 51 | -------------------------------------------------------------------------------- /pkg/sources/github_experimental/repo.go: -------------------------------------------------------------------------------- 1 | package github_experimental 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | "sync" 7 | 8 | "github.com/google/go-github/v67/github" 9 | 10 | "github.com/trufflesecurity/trufflehog/v3/pkg/giturl" 11 | "github.com/trufflesecurity/trufflehog/v3/pkg/pb/source_metadatapb" 12 | ) 13 | 14 | type repoInfoCache struct { 15 | mu sync.RWMutex 16 | cache map[string]repoInfo 17 | } 18 | 19 | func newRepoInfoCache() repoInfoCache { 20 | return repoInfoCache{ 21 | cache: make(map[string]repoInfo), 22 | } 23 | } 24 | 25 | func (r *repoInfoCache) put(repoURL string, info repoInfo) { 26 | r.mu.Lock() 27 | defer r.mu.Unlock() 28 | r.cache[repoURL] = info 29 | } 30 | 31 | func (r *repoInfoCache) get(repoURL string) (repoInfo, bool) { 32 | r.mu.RLock() 33 | defer r.mu.RUnlock() 34 | 35 | info, ok := r.cache[repoURL] 36 | return info, ok 37 | } 38 | 39 | type repoInfo struct { 40 | owner string 41 | name string 42 | fullName string 43 | hasWiki bool // the repo is _likely_ to have a wiki (see the comment on wikiIsReachable func). 44 | size int 45 | visibility source_metadatapb.Visibility 46 | } 47 | 48 | func (s *Source) cacheRepoInfo(r *github.Repository) { 49 | info := repoInfo{ 50 | owner: r.GetOwner().GetLogin(), 51 | name: r.GetName(), 52 | fullName: r.GetFullName(), 53 | hasWiki: r.GetHasWiki(), 54 | size: r.GetSize(), 55 | } 56 | if r.GetPrivate() { 57 | info.visibility = source_metadatapb.Visibility_private 58 | } else { 59 | info.visibility = source_metadatapb.Visibility_public 60 | } 61 | s.repoInfoCache.put(r.GetCloneURL(), info) 62 | } 63 | 64 | func (s *Source) normalizeRepo(repo string) (string, error) { 65 | // If there's a '/', assume it's a URL and try to normalize it. 66 | if strings.ContainsRune(repo, '/') { 67 | return giturl.NormalizeGithubRepo(repo) 68 | } 69 | 70 | return "", fmt.Errorf("no repositories found for %s", repo) 71 | } 72 | -------------------------------------------------------------------------------- /pkg/sources/gitlab/metrics.go: -------------------------------------------------------------------------------- 1 | package gitlab 2 | 3 | import ( 4 | "github.com/prometheus/client_golang/prometheus" 5 | "github.com/prometheus/client_golang/prometheus/promauto" 6 | 7 | "github.com/trufflesecurity/trufflehog/v3/pkg/common" 8 | ) 9 | 10 | var ( 11 | gitlabGroupsEnumerated = promauto.NewGaugeVec(prometheus.GaugeOpts{ 12 | Namespace: common.MetricsNamespace, 13 | Subsystem: common.MetricsSubsystem, 14 | Name: "gitlab_groups_enumerated", 15 | Help: "Total number of GitLab groups enumerated.", 16 | }, 17 | []string{"source_name"}) 18 | 19 | gitlabReposEnumerated = promauto.NewGaugeVec(prometheus.GaugeOpts{ 20 | Namespace: common.MetricsNamespace, 21 | Subsystem: common.MetricsSubsystem, 22 | Name: "gitlab_repos_enumerated", 23 | Help: "Total number of Gitlab repositories enumerated.", 24 | }, 25 | []string{"source_name"}) 26 | 27 | gitlabReposScanned = promauto.NewGaugeVec(prometheus.GaugeOpts{ 28 | Namespace: common.MetricsNamespace, 29 | Subsystem: common.MetricsSubsystem, 30 | Name: "gitlab_repos_scanned", 31 | Help: "Total number of Gitlab repositories scanned.", 32 | }, 33 | []string{"source_name"}) 34 | ) 35 | -------------------------------------------------------------------------------- /pkg/sources/huggingface/repo.go: -------------------------------------------------------------------------------- 1 | package huggingface 2 | 3 | import ( 4 | "fmt" 5 | "sync" 6 | 7 | gogit "github.com/go-git/go-git/v5" 8 | "github.com/trufflesecurity/trufflehog/v3/pkg/context" 9 | "github.com/trufflesecurity/trufflehog/v3/pkg/pb/source_metadatapb" 10 | "github.com/trufflesecurity/trufflehog/v3/pkg/pb/sourcespb" 11 | "github.com/trufflesecurity/trufflehog/v3/pkg/sources/git" 12 | ) 13 | 14 | type repoInfoCache struct { 15 | mu sync.RWMutex 16 | cache map[string]repoInfo 17 | } 18 | 19 | func newRepoInfoCache() repoInfoCache { 20 | return repoInfoCache{ 21 | cache: make(map[string]repoInfo), 22 | } 23 | } 24 | 25 | func (r *repoInfoCache) put(repoURL string, info repoInfo) { 26 | r.mu.Lock() 27 | defer r.mu.Unlock() 28 | r.cache[repoURL] = info 29 | } 30 | 31 | func (r *repoInfoCache) get(repoURL string) (repoInfo, bool) { 32 | r.mu.RLock() 33 | defer r.mu.RUnlock() 34 | 35 | info, ok := r.cache[repoURL] 36 | return info, ok 37 | } 38 | 39 | type repoInfo struct { 40 | owner string 41 | name string 42 | fullName string 43 | visibility source_metadatapb.Visibility 44 | resourceType resourceType 45 | } 46 | 47 | func (s *Source) cloneRepo( 48 | ctx context.Context, 49 | repoURL string, 50 | ) (string, *gogit.Repository, error) { 51 | var ( 52 | path string 53 | repo *gogit.Repository 54 | err error 55 | ) 56 | 57 | switch s.conn.GetCredential().(type) { 58 | case *sourcespb.Huggingface_Unauthenticated: 59 | path, repo, err = git.CloneRepoUsingUnauthenticated(ctx, repoURL) 60 | if err != nil { 61 | return "", nil, err 62 | } 63 | case *sourcespb.Huggingface_Token: 64 | path, repo, err = git.CloneRepoUsingToken(ctx, s.huggingfaceToken, repoURL, "", true) 65 | if err != nil { 66 | return "", nil, err 67 | } 68 | default: 69 | return "", nil, fmt.Errorf("unhandled credential type for repo %s: %T", repoURL, s.conn.GetCredential()) 70 | } 71 | return path, repo, nil 72 | } 73 | -------------------------------------------------------------------------------- /pkg/sources/legacy_reporters.go: -------------------------------------------------------------------------------- 1 | package sources 2 | 3 | import ( 4 | "github.com/trufflesecurity/trufflehog/v3/pkg/common" 5 | "github.com/trufflesecurity/trufflehog/v3/pkg/context" 6 | ) 7 | 8 | var _ ChunkReporter = (*ChanReporter)(nil) 9 | 10 | // ChanReporter is a ChunkReporter that writes to a channel. 11 | type ChanReporter struct { 12 | Ch chan<- *Chunk 13 | } 14 | 15 | func (c ChanReporter) ChunkOk(ctx context.Context, chunk Chunk) error { 16 | return common.CancellableWrite(ctx, c.Ch, &chunk) 17 | } 18 | 19 | func (ChanReporter) ChunkErr(ctx context.Context, err error) error { 20 | ctx.Logger().Error(err, "error chunking") 21 | return ctx.Err() 22 | } 23 | 24 | var _ UnitReporter = (*VisitorReporter)(nil) 25 | 26 | // VisitorReporter is a UnitReporter that will call the provided callbacks for 27 | // finding units and reporting errors. VisitErr is optional; if unset it will 28 | // log the error. 29 | type VisitorReporter struct { 30 | VisitUnit func(context.Context, SourceUnit) error 31 | VisitErr func(context.Context, error) error 32 | } 33 | 34 | func (v VisitorReporter) UnitOk(ctx context.Context, unit SourceUnit) error { 35 | return v.VisitUnit(ctx, unit) 36 | } 37 | 38 | func (v VisitorReporter) UnitErr(ctx context.Context, err error) error { 39 | if v.VisitErr == nil { 40 | ctx.Logger().Error(err, "error enumerating") 41 | return ctx.Err() 42 | } 43 | return v.VisitErr(ctx, err) 44 | } 45 | -------------------------------------------------------------------------------- /pkg/sources/metrics.go: -------------------------------------------------------------------------------- 1 | package sources 2 | 3 | import ( 4 | "github.com/prometheus/client_golang/prometheus" 5 | "github.com/prometheus/client_golang/prometheus/promauto" 6 | "github.com/trufflesecurity/trufflehog/v3/pkg/common" 7 | ) 8 | 9 | var ( 10 | hooksExecTime = promauto.NewHistogramVec(prometheus.HistogramOpts{ 11 | Namespace: common.MetricsNamespace, 12 | Subsystem: common.MetricsSubsystem, 13 | Name: "hooks_exec_time_ms", 14 | Help: "Time spent executing hooks (ms)", 15 | Buckets: []float64{5, 50, 500, 1000}, 16 | }, nil) 17 | 18 | hooksChannelSize = promauto.NewGaugeVec(prometheus.GaugeOpts{ 19 | Namespace: common.MetricsNamespace, 20 | Subsystem: common.MetricsSubsystem, 21 | Name: "hooks_channel_size", 22 | Help: "Total number of metrics waiting in the finished channel.", 23 | }, nil) 24 | ) 25 | -------------------------------------------------------------------------------- /pkg/sources/postman/metrics.go: -------------------------------------------------------------------------------- 1 | package postman 2 | 3 | import ( 4 | "github.com/prometheus/client_golang/prometheus" 5 | "github.com/prometheus/client_golang/prometheus/promauto" 6 | "github.com/trufflesecurity/trufflehog/v3/pkg/common" 7 | ) 8 | 9 | type metrics struct { 10 | apiRequests *prometheus.CounterVec 11 | apiMonthlyRequestsRemaining *prometheus.GaugeVec 12 | } 13 | 14 | var ( 15 | postmanAPIRequestsMetric = promauto.NewCounterVec(prometheus.CounterOpts{ 16 | Namespace: common.MetricsNamespace, 17 | Subsystem: common.MetricsSubsystem, 18 | Name: "postman_api_requests", 19 | Help: "Total number of API requests made to Postman.", 20 | }, 21 | []string{"source_name", "endpoint"}) 22 | 23 | postmanAPIMonthlyRequestsRemaining = promauto.NewGaugeVec(prometheus.GaugeOpts{ 24 | Namespace: common.MetricsNamespace, 25 | Subsystem: common.MetricsSubsystem, 26 | Name: "postman_api_monthly_requests_remaining", 27 | Help: "Total number Postman API requests remaining this month.", 28 | }, 29 | []string{"source_name"}) 30 | ) 31 | 32 | func newMetrics(sourceName string) *metrics { 33 | return &metrics{ 34 | apiRequests: postmanAPIRequestsMetric.MustCurryWith(map[string]string{ 35 | "source_name": sourceName, 36 | }), 37 | apiMonthlyRequestsRemaining: postmanAPIMonthlyRequestsRemaining.MustCurryWith(map[string]string{ 38 | "source_name": sourceName, 39 | }), 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /pkg/sources/source_unit.go: -------------------------------------------------------------------------------- 1 | package sources 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | ) 7 | 8 | // Ensure CommonSourceUnit implements SourceUnit at compile time. 9 | var _ SourceUnit = CommonSourceUnit{} 10 | 11 | // CommonSourceUnit is a common implementation of SourceUnit that Sources can 12 | // use instead of implementing their own types. 13 | type CommonSourceUnit struct { 14 | Kind SourceUnitKind `json:"kind,omitempty"` 15 | ID string `json:"id"` 16 | } 17 | 18 | // SourceUnitID implements the SourceUnit interface. 19 | func (c CommonSourceUnit) SourceUnitID() (string, SourceUnitKind) { 20 | kind := SourceUnitKind("unit") 21 | if c.Kind != "" { 22 | kind = c.Kind 23 | } 24 | return c.ID, kind 25 | } 26 | 27 | func (c CommonSourceUnit) Display() string { 28 | return c.ID 29 | } 30 | 31 | // CommonSourceUnitUnmarshaller is an implementation of SourceUnitUnmarshaller 32 | // for the CommonSourceUnit. A source can embed this struct to gain the 33 | // functionality of converting []byte to a CommonSourceUnit. 34 | type CommonSourceUnitUnmarshaller struct{} 35 | 36 | // UnmarshalSourceUnit implements the SourceUnitUnmarshaller interface. 37 | func (c CommonSourceUnitUnmarshaller) UnmarshalSourceUnit(data []byte) (SourceUnit, error) { 38 | var unit CommonSourceUnit 39 | if err := json.Unmarshal(data, &unit); err != nil { 40 | return nil, err 41 | } 42 | if unit.ID == "" { 43 | return nil, fmt.Errorf("not a CommonSourceUnit") 44 | } 45 | return unit, nil 46 | } 47 | -------------------------------------------------------------------------------- /pkg/sources/sources_test.go: -------------------------------------------------------------------------------- 1 | package sources 2 | 3 | import ( 4 | "testing" 5 | "unsafe" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | // TestChunkSize ensures that the Chunk struct does not exceed 80 bytes. 11 | func TestChunkSize(t *testing.T) { 12 | t.Parallel() 13 | assert.Equal(t, unsafe.Sizeof(Chunk{}), uintptr(80), "Chunk struct size exceeds 80 bytes") 14 | } 15 | -------------------------------------------------------------------------------- /pkg/sources/test_helpers.go: -------------------------------------------------------------------------------- 1 | package sources 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "time" 7 | ) 8 | 9 | type ChunkFunc func(chunk *Chunk) error 10 | 11 | var MatchError = errors.New("chunk doesn't match") 12 | 13 | func HandleTestChannel(chunksCh chan *Chunk, cf ChunkFunc) error { 14 | for { 15 | select { 16 | case gotChunk := <-chunksCh: 17 | err := cf(gotChunk) 18 | if err != nil { 19 | if errors.Is(err, MatchError) { 20 | continue 21 | } 22 | return err 23 | } 24 | return nil 25 | case <-time.After(10 * time.Second): 26 | return fmt.Errorf("no new chunks received after 10 seconds") 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /pkg/sourcestest/sourcestest.go: -------------------------------------------------------------------------------- 1 | package sourcestest 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/trufflesecurity/trufflehog/v3/pkg/context" 7 | "github.com/trufflesecurity/trufflehog/v3/pkg/sources" 8 | ) 9 | 10 | type reporter interface { 11 | sources.UnitReporter 12 | sources.ChunkReporter 13 | } 14 | 15 | var ( 16 | _ reporter = (*TestReporter)(nil) 17 | _ reporter = (*ErrReporter)(nil) 18 | ) 19 | 20 | // TestReporter is a helper struct that implements both UnitReporter and 21 | // ChunkReporter by simply recording the values passed in the methods. 22 | type TestReporter struct { 23 | Units []sources.SourceUnit 24 | UnitErrs []error 25 | Chunks []sources.Chunk 26 | ChunkErrs []error 27 | } 28 | 29 | func (t *TestReporter) UnitOk(_ context.Context, unit sources.SourceUnit) error { 30 | t.Units = append(t.Units, unit) 31 | return nil 32 | } 33 | func (t *TestReporter) UnitErr(_ context.Context, err error) error { 34 | t.UnitErrs = append(t.UnitErrs, err) 35 | return nil 36 | } 37 | func (t *TestReporter) ChunkOk(_ context.Context, chunk sources.Chunk) error { 38 | t.Chunks = append(t.Chunks, chunk) 39 | return nil 40 | } 41 | func (t *TestReporter) ChunkErr(_ context.Context, err error) error { 42 | t.ChunkErrs = append(t.ChunkErrs, err) 43 | return nil 44 | } 45 | 46 | // ErrReporter implements UnitReporter and ChunkReporter but always returns an 47 | // error. 48 | type ErrReporter struct{} 49 | 50 | func (ErrReporter) UnitOk(context.Context, sources.SourceUnit) error { 51 | return fmt.Errorf("ErrReporter: UnitOk error") 52 | } 53 | func (ErrReporter) UnitErr(context.Context, error) error { 54 | return fmt.Errorf("ErrReporter: UnitErr error") 55 | } 56 | func (ErrReporter) ChunkOk(context.Context, sources.Chunk) error { 57 | return fmt.Errorf("ErrReporter: ChunkOk error") 58 | } 59 | func (ErrReporter) ChunkErr(context.Context, error) error { 60 | return fmt.Errorf("ErrReporter: ChunkErr error") 61 | } 62 | -------------------------------------------------------------------------------- /pkg/tui/common/common.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "github.com/aymanbagabas/go-osc52" 5 | zone "github.com/lrstanley/bubblezone" 6 | "github.com/trufflesecurity/trufflehog/v3/pkg/tui/keymap" 7 | "github.com/trufflesecurity/trufflehog/v3/pkg/tui/styles" 8 | ) 9 | 10 | // Common is a struct all components should embed. 11 | type Common struct { 12 | Copy *osc52.Output 13 | Styles *styles.Styles 14 | KeyMap *keymap.KeyMap 15 | Width int 16 | Height int 17 | Zone *zone.Manager 18 | } 19 | 20 | // SetSize sets the width and height of the common struct. 21 | func (c *Common) SetSize(width, height int) { 22 | c.Width = width 23 | c.Height = height 24 | } 25 | -------------------------------------------------------------------------------- /pkg/tui/common/component.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "github.com/charmbracelet/bubbles/help" 5 | tea "github.com/charmbracelet/bubbletea" 6 | ) 7 | 8 | // Component represents a Bubble Tea model that implements a SetSize function. 9 | type Component interface { 10 | tea.Model 11 | help.KeyMap 12 | SetSize(width, height int) 13 | } 14 | -------------------------------------------------------------------------------- /pkg/tui/common/error.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import tea "github.com/charmbracelet/bubbletea" 4 | 5 | // ErrorMsg is a Bubble Tea message that represents an error. 6 | type ErrorMsg error 7 | 8 | // ErrorCmd returns an ErrorMsg from error. 9 | func ErrorCmd(err error) tea.Cmd { 10 | return func() tea.Msg { 11 | return ErrorMsg(err) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /pkg/tui/common/style.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "github.com/charmbracelet/glamour" 5 | gansi "github.com/charmbracelet/glamour/ansi" 6 | ) 7 | 8 | func strptr(s string) *string { 9 | return &s 10 | } 11 | 12 | // StyleConfig returns the default Glamour style configuration. 13 | func StyleConfig() gansi.StyleConfig { 14 | noColor := strptr("") 15 | s := glamour.DarkStyleConfig 16 | s.H1.BackgroundColor = noColor 17 | s.H1.Prefix = "# " 18 | s.H1.Suffix = "" 19 | s.H1.Color = strptr("39") 20 | s.Document.StylePrimitive.Color = noColor 21 | s.CodeBlock.Chroma.Text.Color = noColor 22 | s.CodeBlock.Chroma.Name.Color = noColor 23 | // This fixes an issue with the default style config. For example 24 | // highlighting empty spaces with red in Dockerfile type. 25 | s.CodeBlock.Chroma.Error.BackgroundColor = noColor 26 | return s 27 | } 28 | -------------------------------------------------------------------------------- /pkg/tui/common/utils.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/muesli/reflow/truncate" 7 | "github.com/trufflesecurity/trufflehog/v3/pkg/tui/components/textinputs" 8 | ) 9 | 10 | // TruncateString is a convenient wrapper around truncate.TruncateString. 11 | func TruncateString(s string, max int) string { 12 | if max < 0 { 13 | max = 0 14 | } 15 | return truncate.StringWithTail(s, uint(max), "…") 16 | } 17 | 18 | func SummarizeSource(keys []string, inputs map[string]textinputs.Input, labels map[string]string) string { 19 | summary := strings.Builder{} 20 | for _, key := range keys { 21 | if inputs[key].Value != "" { 22 | summary.WriteString("\t" + labels[key] + ": " + inputs[key].Value + "\n") 23 | } 24 | } 25 | 26 | summary.WriteString("\n") 27 | return summary.String() 28 | } 29 | -------------------------------------------------------------------------------- /pkg/tui/components/formfield/formfield.go: -------------------------------------------------------------------------------- 1 | package formfield 2 | 3 | import ( 4 | "strings" 5 | 6 | tea "github.com/charmbracelet/bubbletea" 7 | "github.com/trufflesecurity/trufflehog/v3/pkg/tui/common" 8 | "github.com/trufflesecurity/trufflehog/v3/pkg/tui/styles" 9 | ) 10 | 11 | type FormField struct { 12 | Label string 13 | Required bool 14 | Help string 15 | Component tea.Model 16 | } 17 | 18 | func NewFormField(common common.Common) *FormField { 19 | return &FormField{} 20 | } 21 | 22 | func (field *FormField) ViewLabel() string { 23 | var label strings.Builder 24 | if field.Required { 25 | label.WriteString(styles.BoldTextStyle.Render(field.Label) + "*\n") 26 | } else { 27 | label.WriteString(styles.BoldTextStyle.Render(field.Label) + "\n") 28 | } 29 | 30 | return label.String() 31 | } 32 | 33 | func (field *FormField) ViewHelp() string { 34 | var help strings.Builder 35 | help.WriteString(styles.HintTextStyle.Render(field.Help) + "\n") 36 | 37 | return help.String() 38 | } 39 | -------------------------------------------------------------------------------- /pkg/tui/components/header/header.go: -------------------------------------------------------------------------------- 1 | package header 2 | 3 | import ( 4 | "strings" 5 | 6 | tea "github.com/charmbracelet/bubbletea" 7 | "github.com/trufflesecurity/trufflehog/v3/pkg/tui/common" 8 | ) 9 | 10 | // Header represents a header component. 11 | type Header struct { 12 | common common.Common 13 | text string 14 | } 15 | 16 | // New creates a new header component. 17 | func New(c common.Common, text string) *Header { 18 | return &Header{ 19 | common: c, 20 | text: text, 21 | } 22 | } 23 | 24 | // SetSize implements common.Component. 25 | func (h *Header) SetSize(width, height int) { 26 | h.common.SetSize(width, height) 27 | } 28 | 29 | // Init implements tea.Model. 30 | func (h *Header) Init() tea.Cmd { 31 | return nil 32 | } 33 | 34 | // Update implements tea.Model. 35 | func (h *Header) Update(msg tea.Msg) (tea.Model, tea.Cmd) { 36 | return h, nil 37 | } 38 | 39 | // View implements tea.Model. 40 | func (h *Header) View() string { 41 | return h.common.Styles.ServerName.Render(strings.TrimSpace(h.text)) 42 | } 43 | -------------------------------------------------------------------------------- /pkg/tui/components/textinput/textinput.go: -------------------------------------------------------------------------------- 1 | package textinput 2 | 3 | import ( 4 | "github.com/charmbracelet/bubbles/textinput" 5 | tea "github.com/charmbracelet/bubbletea" 6 | ) 7 | 8 | type ( 9 | errMsg error 10 | ) 11 | 12 | type TextInput struct { 13 | textInput textinput.Model 14 | err error 15 | } 16 | 17 | func New(placeholder string) TextInput { 18 | ti := textinput.New() 19 | ti.Placeholder = placeholder 20 | ti.Focus() 21 | ti.CharLimit = 156 22 | ti.Width = 60 23 | 24 | return TextInput{ 25 | textInput: ti, 26 | err: nil, 27 | } 28 | } 29 | 30 | func (m TextInput) Init() tea.Cmd { 31 | return textinput.Blink 32 | } 33 | 34 | func (m TextInput) Update(msg tea.Msg) (tea.Model, tea.Cmd) { 35 | var cmd tea.Cmd 36 | 37 | switch msg := msg.(type) { 38 | 39 | // We handle errors just like any other message 40 | case errMsg: 41 | m.err = msg 42 | return m, nil 43 | } 44 | 45 | m.textInput, cmd = m.textInput.Update(msg) 46 | return m, cmd 47 | } 48 | 49 | func (m TextInput) View() string { 50 | return m.textInput.View() 51 | } 52 | -------------------------------------------------------------------------------- /pkg/tui/pages/contact_enterprise/contact_enterprise.go: -------------------------------------------------------------------------------- 1 | package contact_enterprise 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/charmbracelet/bubbles/key" 7 | tea "github.com/charmbracelet/bubbletea" 8 | "github.com/charmbracelet/lipgloss" 9 | "github.com/trufflesecurity/trufflehog/v3/pkg/tui/common" 10 | "github.com/trufflesecurity/trufflehog/v3/pkg/tui/styles" 11 | ) 12 | 13 | type ContactEnterprise struct { 14 | common.Common 15 | viewed bool 16 | } 17 | 18 | var ( 19 | linkStyle = lipgloss.NewStyle().Foreground( 20 | lipgloss.Color("28")) // green 21 | ) 22 | 23 | func New(c common.Common) *ContactEnterprise { 24 | return &ContactEnterprise{ 25 | Common: c, 26 | viewed: false, 27 | } 28 | } 29 | 30 | func (m *ContactEnterprise) Init() tea.Cmd { 31 | return nil 32 | } 33 | 34 | func (m *ContactEnterprise) Update(msg tea.Msg) (tea.Model, tea.Cmd) { 35 | if m.viewed { 36 | return m, tea.Quit 37 | } 38 | 39 | return m, func() tea.Msg { return nil } 40 | } 41 | 42 | func (m *ContactEnterprise) View() string { 43 | 44 | s := strings.Builder{} 45 | s.WriteString("Interested in TruffleHog enterprise?\n") 46 | s.WriteString(linkStyle.Render("🔗 https://trufflesecurity.com/contact")) 47 | 48 | m.viewed = true 49 | return styles.AppStyle.Render(s.String()) 50 | } 51 | 52 | func (m *ContactEnterprise) ShortHelp() []key.Binding { 53 | // TODO: actually return something 54 | return nil 55 | } 56 | 57 | func (m *ContactEnterprise) FullHelp() [][]key.Binding { 58 | // TODO: actually return something 59 | return nil 60 | } 61 | -------------------------------------------------------------------------------- /pkg/tui/pages/source_configure/item.go: -------------------------------------------------------------------------------- 1 | package source_configure 2 | 3 | type Item struct { 4 | title string 5 | description string 6 | } 7 | 8 | func (i Item) ID() string { return i.title } 9 | 10 | func (i Item) Title() string { 11 | return i.title 12 | } 13 | func (i Item) Description() string { 14 | return i.description 15 | } 16 | 17 | func (i Item) SetDescription(d string) Item { 18 | i.description = d 19 | return i 20 | } 21 | 22 | // We shouldn't be filtering for these list items. 23 | func (i Item) FilterValue() string { return "" } 24 | -------------------------------------------------------------------------------- /pkg/tui/pages/source_configure/source_component.go: -------------------------------------------------------------------------------- 1 | package source_configure 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/charmbracelet/bubbles/key" 7 | tea "github.com/charmbracelet/bubbletea" 8 | "github.com/trufflesecurity/trufflehog/v3/pkg/tui/common" 9 | "github.com/trufflesecurity/trufflehog/v3/pkg/tui/sources" 10 | "github.com/trufflesecurity/trufflehog/v3/pkg/tui/styles" 11 | ) 12 | 13 | type SourceComponent struct { 14 | common.Common 15 | parent *SourceConfigure 16 | form tea.Model 17 | } 18 | 19 | func NewSourceComponent(common common.Common, parent *SourceConfigure) *SourceComponent { 20 | return &SourceComponent{ 21 | Common: common, 22 | parent: parent, 23 | } 24 | } 25 | 26 | func (m *SourceComponent) SetForm(form tea.Model) { 27 | m.form = form 28 | } 29 | 30 | func (m *SourceComponent) Init() tea.Cmd { 31 | return nil 32 | } 33 | 34 | func (m *SourceComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) { 35 | // TODO: Add a focus variable. 36 | if m.form != nil { 37 | model, cmd := m.form.Update(msg) 38 | m.form = model 39 | return m, cmd 40 | } 41 | return m, nil 42 | } 43 | 44 | func (m *SourceComponent) View() string { 45 | var view strings.Builder 46 | 47 | view.WriteString(styles.BoldTextStyle.Render("\nConfiguring "+styles.PrimaryTextStyle.Render(m.parent.configTabSource)) + "\n") 48 | 49 | view.WriteString(styles.HintTextStyle.Render("* required field") + "\n\n") 50 | 51 | sourceNote := sources.GetSourceNotes(m.parent.configTabSource) 52 | if len(sourceNote) > 0 { 53 | view.WriteString("⭐ " + sourceNote + " ⭐\n\n") 54 | } 55 | 56 | if m.form != nil { 57 | view.WriteString(m.form.View()) 58 | view.WriteString("\n") 59 | } 60 | return view.String() 61 | } 62 | 63 | func (m *SourceComponent) ShortHelp() []key.Binding { 64 | // TODO: actually return something 65 | return nil 66 | } 67 | 68 | func (m *SourceComponent) FullHelp() [][]key.Binding { 69 | // TODO: actually return something 70 | return nil 71 | } 72 | -------------------------------------------------------------------------------- /pkg/tui/pages/source_configure/trufflehog_component.go: -------------------------------------------------------------------------------- 1 | package source_configure 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/charmbracelet/bubbles/key" 7 | tea "github.com/charmbracelet/bubbletea" 8 | "github.com/trufflesecurity/trufflehog/v3/pkg/tui/common" 9 | "github.com/trufflesecurity/trufflehog/v3/pkg/tui/styles" 10 | ) 11 | 12 | type TrufflehogComponent struct { 13 | common.Common 14 | parent *SourceConfigure 15 | form tea.Model 16 | } 17 | 18 | func NewTrufflehogComponent(common common.Common, parent *SourceConfigure) *TrufflehogComponent { 19 | return &TrufflehogComponent{ 20 | Common: common, 21 | parent: parent, 22 | } 23 | } 24 | 25 | func (m *TrufflehogComponent) SetForm(form tea.Model) { 26 | m.form = form 27 | } 28 | 29 | func (m *TrufflehogComponent) Init() tea.Cmd { 30 | return nil 31 | } 32 | 33 | func (m *TrufflehogComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) { 34 | // TODO: Add a focus variable. 35 | if m.form != nil { 36 | model, cmd := m.form.Update(msg) 37 | m.form = model 38 | 39 | return m, cmd 40 | } 41 | return m, nil 42 | } 43 | 44 | func (m *TrufflehogComponent) View() string { 45 | var view strings.Builder 46 | 47 | view.WriteString(styles.BoldTextStyle.Render("\nConfiguring "+styles.PrimaryTextStyle.Render("TruffleHog")) + "\n") 48 | view.WriteString(styles.HintTextStyle.Render("You can skip this completely and run with defaults") + "\n\n") 49 | 50 | if m.form != nil { 51 | view.WriteString(m.form.View()) 52 | view.WriteString("\n") 53 | } 54 | 55 | return view.String() 56 | } 57 | 58 | func (m *TrufflehogComponent) ShortHelp() []key.Binding { 59 | // TODO: actually return something 60 | return nil 61 | } 62 | 63 | func (m *TrufflehogComponent) FullHelp() [][]key.Binding { 64 | // TODO: actually return something 65 | return nil 66 | } 67 | -------------------------------------------------------------------------------- /pkg/tui/pages/source_select/item.go: -------------------------------------------------------------------------------- 1 | package source_select 2 | 3 | type SourceItem struct { 4 | title string 5 | description string 6 | enterprise bool 7 | } 8 | 9 | func OssItem(title, description string) SourceItem { 10 | return SourceItem{title, description, false} 11 | } 12 | 13 | func EnterpriseItem(title, description string) SourceItem { 14 | return SourceItem{title, description, true} 15 | } 16 | 17 | func (i SourceItem) ID() string { return i.title } 18 | 19 | func (i SourceItem) Title() string { 20 | if i.enterprise { 21 | return "💸 " + i.title 22 | } 23 | return i.title 24 | } 25 | func (i SourceItem) Description() string { 26 | if i.enterprise { 27 | return i.description + " (Enterprise only)" 28 | } 29 | return i.description 30 | } 31 | 32 | func (i SourceItem) FilterValue() string { return i.title + i.description } 33 | -------------------------------------------------------------------------------- /pkg/tui/pages/view_oss/view_oss.go: -------------------------------------------------------------------------------- 1 | package view_oss 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/charmbracelet/bubbles/key" 7 | tea "github.com/charmbracelet/bubbletea" 8 | "github.com/charmbracelet/lipgloss" 9 | "github.com/trufflesecurity/trufflehog/v3/pkg/tui/common" 10 | "github.com/trufflesecurity/trufflehog/v3/pkg/tui/styles" 11 | ) 12 | 13 | type ViewOSS struct { 14 | common.Common 15 | viewed bool 16 | } 17 | 18 | var ( 19 | linkStyle = lipgloss.NewStyle().Foreground( 20 | lipgloss.Color("28")) // green 21 | ) 22 | 23 | func New(c common.Common) *ViewOSS { 24 | return &ViewOSS{ 25 | Common: c, 26 | viewed: false, 27 | } 28 | } 29 | 30 | func (m *ViewOSS) Init() tea.Cmd { 31 | return nil 32 | } 33 | 34 | func (m *ViewOSS) Update(msg tea.Msg) (tea.Model, tea.Cmd) { 35 | if m.viewed { 36 | return m, tea.Quit 37 | } 38 | 39 | return m, func() tea.Msg { return nil } 40 | } 41 | 42 | func (m *ViewOSS) View() string { 43 | s := strings.Builder{} 44 | s.WriteString("View our open-source project on GitHub\n") 45 | s.WriteString(linkStyle.Render("🔗 https://github.com/trufflesecurity/trufflehog ")) 46 | 47 | m.viewed = true 48 | return styles.AppStyle.Render(s.String()) 49 | } 50 | 51 | func (m *ViewOSS) ShortHelp() []key.Binding { 52 | // TODO: actually return something 53 | return nil 54 | } 55 | 56 | func (m *ViewOSS) FullHelp() [][]key.Binding { 57 | // TODO: actually return something 58 | return nil 59 | } 60 | -------------------------------------------------------------------------------- /pkg/tui/sources/circleci/circleci.go: -------------------------------------------------------------------------------- 1 | package circleci 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/trufflesecurity/trufflehog/v3/pkg/tui/common" 7 | "github.com/trufflesecurity/trufflehog/v3/pkg/tui/components/textinputs" 8 | ) 9 | 10 | type circleCiCmdModel struct { 11 | textinputs.Model 12 | } 13 | 14 | func GetFields() circleCiCmdModel { 15 | token := textinputs.InputConfig{ 16 | Label: "API Token", 17 | Key: "token", 18 | Required: true, 19 | Placeholder: "top secret token", 20 | } 21 | 22 | return circleCiCmdModel{textinputs.New([]textinputs.InputConfig{token})} 23 | } 24 | 25 | func (m circleCiCmdModel) Cmd() string { 26 | var command []string 27 | command = append(command, "trufflehog", "circleci") 28 | 29 | inputs := m.GetInputs() 30 | command = append(command, "--token="+inputs["token"].Value) 31 | 32 | return strings.Join(command, " ") 33 | } 34 | 35 | func (m circleCiCmdModel) Summary() string { 36 | inputs := m.GetInputs() 37 | labels := m.GetLabels() 38 | keys := []string{"token"} 39 | 40 | return common.SummarizeSource(keys, inputs, labels) 41 | } 42 | -------------------------------------------------------------------------------- /pkg/tui/sources/docker/docker.go: -------------------------------------------------------------------------------- 1 | package docker 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/trufflesecurity/trufflehog/v3/pkg/tui/common" 7 | "github.com/trufflesecurity/trufflehog/v3/pkg/tui/components/textinputs" 8 | ) 9 | 10 | type dockerCmdModel struct { 11 | textinputs.Model 12 | } 13 | 14 | func GetFields() dockerCmdModel { 15 | images := textinputs.InputConfig{ 16 | Label: "Docker image(s)", 17 | Key: "images", 18 | Required: true, 19 | Help: "Separate by space if multiple.", 20 | Placeholder: "trufflesecurity/secrets", 21 | } 22 | 23 | return dockerCmdModel{textinputs.New([]textinputs.InputConfig{images})} 24 | } 25 | 26 | func (m dockerCmdModel) Cmd() string { 27 | 28 | var command []string 29 | command = append(command, "trufflehog", "docker") 30 | 31 | inputs := m.GetInputs() 32 | vals := inputs["images"].Value 33 | 34 | if vals != "" { 35 | images := strings.Fields(vals) 36 | for _, image := range images { 37 | command = append(command, "--image="+image) 38 | } 39 | } 40 | 41 | return strings.Join(command, " ") 42 | } 43 | 44 | func (m dockerCmdModel) Summary() string { 45 | inputs := m.GetInputs() 46 | labels := m.GetLabels() 47 | keys := []string{"images"} 48 | 49 | return common.SummarizeSource(keys, inputs, labels) 50 | } 51 | -------------------------------------------------------------------------------- /pkg/tui/sources/filesystem/filesystem.go: -------------------------------------------------------------------------------- 1 | package filesystem 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/trufflesecurity/trufflehog/v3/pkg/tui/common" 7 | "github.com/trufflesecurity/trufflehog/v3/pkg/tui/components/textinputs" 8 | ) 9 | 10 | type fsModel struct { 11 | textinputs.Model 12 | } 13 | 14 | func GetFields() fsModel { 15 | path := textinputs.InputConfig{ 16 | Label: "Path", 17 | Key: "path", 18 | Required: true, 19 | Help: "Files and directories to scan. Separate by space if multiple.", 20 | Placeholder: "path/to/file.txt path/to/another/dir", 21 | } 22 | 23 | return fsModel{textinputs.New([]textinputs.InputConfig{path})} 24 | } 25 | 26 | func (m fsModel) Cmd() string { 27 | var command []string 28 | command = append(command, "trufflehog", "filesystem") 29 | 30 | inputs := m.GetInputs() 31 | command = append(command, inputs["path"].Value) 32 | 33 | return strings.Join(command, " ") 34 | } 35 | 36 | func (m fsModel) Summary() string { 37 | inputs := m.GetInputs() 38 | labels := m.GetLabels() 39 | 40 | keys := []string{"path"} 41 | return common.SummarizeSource(keys, inputs, labels) 42 | } 43 | -------------------------------------------------------------------------------- /pkg/tui/sources/gcs/gcs.go: -------------------------------------------------------------------------------- 1 | package gcs 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/trufflesecurity/trufflehog/v3/pkg/tui/common" 7 | "github.com/trufflesecurity/trufflehog/v3/pkg/tui/components/textinputs" 8 | ) 9 | 10 | type gcsCmdModel struct { 11 | textinputs.Model 12 | } 13 | 14 | func GetFields() gcsCmdModel { 15 | projectId := textinputs.InputConfig{ 16 | Label: "Project ID", 17 | Key: "project-id", 18 | Required: true, 19 | Placeholder: "trufflehog-testing", 20 | } 21 | 22 | return gcsCmdModel{textinputs.New([]textinputs.InputConfig{projectId})} 23 | } 24 | 25 | func (m gcsCmdModel) Cmd() string { 26 | var command []string 27 | command = append(command, "trufflehog", "gcs") 28 | 29 | inputs := m.GetInputs() 30 | 31 | command = append(command, "--project-id="+inputs["project-id"].Value) 32 | 33 | command = append(command, "--cloud-environment") 34 | return strings.Join(command, " ") 35 | } 36 | 37 | func (m gcsCmdModel) Summary() string { 38 | inputs := m.GetInputs() 39 | labels := m.GetLabels() 40 | 41 | keys := []string{"project-id"} 42 | return common.SummarizeSource(keys, inputs, labels) 43 | } 44 | -------------------------------------------------------------------------------- /pkg/tui/sources/git/git.go: -------------------------------------------------------------------------------- 1 | package git 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/trufflesecurity/trufflehog/v3/pkg/tui/common" 7 | "github.com/trufflesecurity/trufflehog/v3/pkg/tui/components/textinputs" 8 | ) 9 | 10 | type gitCmdModel struct { 11 | textinputs.Model 12 | } 13 | 14 | func GetFields() gitCmdModel { 15 | uri := textinputs.InputConfig{ 16 | Label: "Git URI", 17 | Key: "uri", 18 | Help: "file:// for local git repos", 19 | Required: true, 20 | Placeholder: "git@github.com:trufflesecurity/trufflehog.git", 21 | } 22 | 23 | return gitCmdModel{textinputs.New([]textinputs.InputConfig{uri})} 24 | } 25 | 26 | func (m gitCmdModel) Cmd() string { 27 | var command []string 28 | command = append(command, "trufflehog", "git") 29 | 30 | inputs := m.GetInputs() 31 | 32 | command = append(command, inputs["uri"].Value) 33 | 34 | return strings.Join(command, " ") 35 | } 36 | 37 | func (m gitCmdModel) Summary() string { 38 | inputs := m.GetInputs() 39 | labels := m.GetLabels() 40 | 41 | keys := []string{"uri"} 42 | return common.SummarizeSource(keys, inputs, labels) 43 | } 44 | -------------------------------------------------------------------------------- /pkg/tui/sources/gitlab/gitlab.go: -------------------------------------------------------------------------------- 1 | package gitlab 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/trufflesecurity/trufflehog/v3/pkg/tui/common" 7 | "github.com/trufflesecurity/trufflehog/v3/pkg/tui/components/textinputs" 8 | ) 9 | 10 | type gitlabCmdModel struct { 11 | textinputs.Model 12 | } 13 | 14 | func GetFields() gitlabCmdModel { 15 | token := textinputs.InputConfig{ 16 | Label: "GitLab token", 17 | Key: "token", 18 | Required: true, 19 | Help: "Personal access token with read access", 20 | Placeholder: "glpat-", 21 | } 22 | 23 | return gitlabCmdModel{textinputs.New([]textinputs.InputConfig{token})} 24 | } 25 | 26 | func (m gitlabCmdModel) Cmd() string { 27 | var command []string 28 | command = append(command, "trufflehog", "gitlab") 29 | 30 | inputs := m.GetInputs() 31 | 32 | command = append(command, "--token="+inputs["token"].Value) 33 | 34 | return strings.Join(command, " ") 35 | } 36 | 37 | func (m gitlabCmdModel) Summary() string { 38 | inputs := m.GetInputs() 39 | labels := m.GetLabels() 40 | 41 | keys := []string{"token"} 42 | return common.SummarizeSource(keys, inputs, labels) 43 | } 44 | -------------------------------------------------------------------------------- /pkg/tui/sources/s3/s3.go: -------------------------------------------------------------------------------- 1 | package s3 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/trufflesecurity/trufflehog/v3/pkg/tui/common" 7 | "github.com/trufflesecurity/trufflehog/v3/pkg/tui/components/textinputs" 8 | ) 9 | 10 | type s3CmdModel struct { 11 | textinputs.Model 12 | } 13 | 14 | func GetFields() s3CmdModel { 15 | bucket := textinputs.InputConfig{ 16 | Label: "S3 bucket name(s)", 17 | Key: "buckets", 18 | Required: true, 19 | Placeholder: "truffletestbucket", 20 | Help: "Buckets to scan. Separate by space if multiple.", 21 | } 22 | 23 | return s3CmdModel{textinputs.New([]textinputs.InputConfig{bucket})} 24 | } 25 | 26 | func (m s3CmdModel) Cmd() string { 27 | var command []string 28 | command = append(command, "trufflehog", "s3") 29 | 30 | inputs := m.GetInputs() 31 | vals := inputs["buckets"].Value 32 | if vals != "" { 33 | buckets := strings.Fields(vals) 34 | for _, bucket := range buckets { 35 | command = append(command, "--bucket="+bucket) 36 | } 37 | } 38 | 39 | return strings.Join(command, " ") 40 | } 41 | 42 | func (m s3CmdModel) Summary() string { 43 | inputs := m.GetInputs() 44 | labels := m.GetLabels() 45 | 46 | keys := []string{"buckets"} 47 | return common.SummarizeSource(keys, inputs, labels) 48 | } 49 | -------------------------------------------------------------------------------- /pkg/verificationcache/in_memory_metrics.go: -------------------------------------------------------------------------------- 1 | package verificationcache 2 | 3 | import ( 4 | "sync/atomic" 5 | "time" 6 | ) 7 | 8 | // InMemoryMetrics is a MetricsReporter that stores reported metrics in memory for retrieval at the end of a scan. 9 | type InMemoryMetrics struct { 10 | CredentialVerificationsSaved atomic.Int32 11 | FromDataVerifyTimeSpentMS atomic.Int64 12 | ResultCacheHits atomic.Int32 13 | ResultCacheHitsWasted atomic.Int32 14 | ResultCacheMisses atomic.Int32 15 | } 16 | 17 | var _ MetricsReporter = (*InMemoryMetrics)(nil) 18 | 19 | func (m *InMemoryMetrics) AddCredentialVerificationsSaved(count int) { 20 | m.CredentialVerificationsSaved.Add(int32(count)) 21 | } 22 | 23 | func (m *InMemoryMetrics) AddFromDataVerifyTimeSpent(wallTime time.Duration) { 24 | m.FromDataVerifyTimeSpentMS.Add(wallTime.Milliseconds()) 25 | } 26 | 27 | func (m *InMemoryMetrics) AddResultCacheHits(count int) { 28 | m.ResultCacheHits.Add(int32(count)) 29 | } 30 | 31 | func (m *InMemoryMetrics) AddResultCacheMisses(count int) { 32 | m.ResultCacheMisses.Add(int32(count)) 33 | } 34 | 35 | func (m *InMemoryMetrics) AddResultCacheHitsWasted(count int) { 36 | m.ResultCacheHitsWasted.Add(int32(count)) 37 | } 38 | -------------------------------------------------------------------------------- /pkg/verificationcache/metrics_reporter.go: -------------------------------------------------------------------------------- 1 | package verificationcache 2 | 3 | import "time" 4 | 5 | // MetricsReporter is an interface used by a verification cache to report various metrics related to its operation. 6 | // Implementations must be thread-safe. 7 | type MetricsReporter interface { 8 | // AddCredentialVerificationsSaved records "saved" verification attempts, which is when credential verification 9 | // status is loaded from the cache instead of retrieved from a remote verification endpoint. This number might be 10 | // smaller than the cache hit count due to cache hit "wasting"; see AddResultCacheHitsWasted for more information. 11 | AddCredentialVerificationsSaved(count int) 12 | 13 | // AddFromDataVerifyTimeSpent records wall time spent in calls to detector.FromData with verify=true. 14 | AddFromDataVerifyTimeSpent(wallTime time.Duration) 15 | 16 | // AddResultCacheHits records result cache hits. Not all cache hits result in elided remote verification requests 17 | // due to cache hit "wasting"; see AddResultCacheHitsWasted for more information. 18 | AddResultCacheHits(count int) 19 | 20 | // AddResultCacheMisses records result cache misses. 21 | AddResultCacheMisses(count int) 22 | 23 | // AddResultCacheHitsWasted records "wasted" result cache hits. A "wasted" result cache hit is a result cache hit 24 | // that does not elide a remote verification request because there are other secret findings in the relevant chunk 25 | // that are not cached. When this happens, the detector's FromData method must be called anyway, so the cache hit 26 | // doesn't save any remote requests. 27 | AddResultCacheHitsWasted(count int) 28 | } 29 | -------------------------------------------------------------------------------- /pkg/verificationcache/result_cache.go: -------------------------------------------------------------------------------- 1 | package verificationcache 2 | 3 | import ( 4 | "github.com/trufflesecurity/trufflehog/v3/pkg/cache" 5 | "github.com/trufflesecurity/trufflehog/v3/pkg/detectors" 6 | ) 7 | 8 | // ResultCache is a cache that holds individual detector results. It serves as a component of a VerificationCache. 9 | type ResultCache cache.Cache[detectors.Result] 10 | -------------------------------------------------------------------------------- /pkg/version/version.go: -------------------------------------------------------------------------------- 1 | package version 2 | 3 | var BuildVersion = "dev" 4 | -------------------------------------------------------------------------------- /pkg/writers/buffer_writer/metrics.go: -------------------------------------------------------------------------------- 1 | package bufferwriter 2 | 3 | import ( 4 | "github.com/prometheus/client_golang/prometheus" 5 | "github.com/prometheus/client_golang/prometheus/promauto" 6 | 7 | "github.com/trufflesecurity/trufflehog/v3/pkg/common" 8 | ) 9 | 10 | var ( 11 | writeSize = promauto.NewHistogram(prometheus.HistogramOpts{ 12 | Namespace: common.MetricsNamespace, 13 | Subsystem: common.MetricsSubsystem, 14 | Name: "buffer_writer_write_size_bytes", 15 | Help: "Total size of data written by the BufferWriter in bytes.", 16 | Buckets: prometheus.ExponentialBuckets(100, 10, 7), 17 | }) 18 | 19 | totalWriteDuration = promauto.NewCounter(prometheus.CounterOpts{ 20 | Namespace: common.MetricsNamespace, 21 | Subsystem: common.MetricsSubsystem, 22 | Name: "buffer_writer_total_write_duration_microseconds", 23 | Help: "Total duration of write operations by the BufferWriter in microseconds.", 24 | }) 25 | ) 26 | -------------------------------------------------------------------------------- /pkg/writers/buffered_file_writer/metrics.go: -------------------------------------------------------------------------------- 1 | package bufferedfilewriter 2 | 3 | import ( 4 | "github.com/prometheus/client_golang/prometheus" 5 | "github.com/prometheus/client_golang/prometheus/promauto" 6 | 7 | "github.com/trufflesecurity/trufflehog/v3/pkg/common" 8 | ) 9 | 10 | var ( 11 | totalWriteSize = promauto.NewCounter(prometheus.CounterOpts{ 12 | Namespace: common.MetricsNamespace, 13 | Subsystem: common.MetricsSubsystem, 14 | Name: "buffered_file_writer_total_write_size_bytes", 15 | Help: "Total size of data written by the BufferedFileWriter in bytes.", 16 | }) 17 | 18 | totalWriteDuration = promauto.NewCounter(prometheus.CounterOpts{ 19 | Namespace: common.MetricsNamespace, 20 | Subsystem: common.MetricsSubsystem, 21 | Name: "buffered_file_writer_total_write_duration_microseconds", 22 | Help: "Total duration of write operations by the BufferedFileWriter in microseconds.", 23 | }) 24 | 25 | diskWriteCount = promauto.NewCounter(prometheus.CounterOpts{ 26 | Namespace: common.MetricsNamespace, 27 | Subsystem: common.MetricsSubsystem, 28 | Name: "disk_write_count", 29 | Help: "Total number of times data was written to disk by the BufferedFileWriter.", 30 | }) 31 | 32 | // The first bucket is greater than the default threshold to avoid a bucket with a zero value. 33 | fileSizeHistogram = promauto.NewHistogram(prometheus.HistogramOpts{ 34 | Namespace: common.MetricsNamespace, 35 | Subsystem: common.MetricsSubsystem, 36 | Name: "file_size_bytes", 37 | Help: "Sizes of files created by the BufferedFileWriter.", 38 | Buckets: prometheus.ExponentialBuckets(defaultThreshold*2, 4, 5), 39 | }) 40 | ) 41 | -------------------------------------------------------------------------------- /proto/credentials.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package credentials; 4 | 5 | option go_package = "github.com/trufflesecurity/trufflehog/v3/pkg/pb/credentialspb"; 6 | 7 | import "validate/validate.proto"; 8 | 9 | message Unauthenticated {} 10 | 11 | message SSHAuth {} 12 | 13 | message CloudEnvironment {} 14 | 15 | message BasicAuth { 16 | string username = 1; 17 | string password = 2; 18 | } 19 | 20 | message Header { 21 | string key = 1; 22 | string value = 2; 23 | } 24 | 25 | message ClientCredentials { 26 | string tenant_id = 1; 27 | string client_id =2; 28 | string client_secret=3; 29 | } 30 | 31 | message ClientCertificate { 32 | string tenant_id = 1; 33 | string client_id =2; 34 | string certificate_path =3; 35 | string certificate_password =4; 36 | } 37 | 38 | message Oauth2 { 39 | string refresh_token = 1; 40 | string client_id = 2; 41 | string client_secret = 3; 42 | string access_token = 4; 43 | } 44 | 45 | message KeySecret { 46 | string key = 1; 47 | string secret = 2; 48 | } 49 | 50 | message AWSSessionTokenSecret { 51 | string key = 1 [(validate.rules).string.min_len = 1]; 52 | string secret = 2 [(validate.rules).string.min_len = 1]; 53 | string session_token = 3 [(validate.rules).string.min_len = 1]; 54 | } 55 | 56 | message AWS { 57 | string key = 1 [(validate.rules).string.min_len = 1]; 58 | string secret = 2 [(validate.rules).string.min_len = 1]; 59 | string region = 3; 60 | } 61 | 62 | message SES { 63 | AWS creds = 1; 64 | string sender = 2; 65 | repeated string recipients = 3; 66 | } 67 | 68 | message GitHubApp { 69 | string private_key = 1; 70 | string installation_id = 2; 71 | string app_id = 3; 72 | } 73 | 74 | message SlackTokens { 75 | string app_token = 1; 76 | string bot_token = 2; 77 | string client_token = 3; 78 | } 79 | -------------------------------------------------------------------------------- /proto/custom_detectors.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package custom_detectors; 4 | 5 | option go_package = "github.com/trufflesecurity/trufflehog/v3/pkg/pb/custom_detectorspb"; 6 | 7 | import "validate/validate.proto"; 8 | 9 | message CustomDetectors { 10 | repeated CustomRegex detectors = 1; 11 | } 12 | 13 | message CustomRegex { 14 | string name = 1; 15 | repeated string keywords = 2; 16 | map regex = 3; 17 | repeated VerifierConfig verify = 4; 18 | string description = 5; 19 | repeated string exclude_regexes_capture = 6; 20 | repeated string exclude_words = 7; 21 | float entropy = 8; 22 | repeated string exclude_regexes_match = 9; 23 | string primary_regex_name = 10; 24 | } 25 | 26 | 27 | message VerifierConfig { 28 | string endpoint = 1 [(validate.rules).string.uri_ref = true]; 29 | bool unsafe = 2; 30 | repeated string headers = 3; 31 | repeated string successRanges = 4; 32 | } 33 | -------------------------------------------------------------------------------- /scripts/gen_proto.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -eux 4 | 5 | for pbfile in $(ls proto/); do 6 | mod=${pbfile%%.proto} 7 | protoc -I proto/ \ 8 | -I ${GOPATH}/src \ 9 | -I /usr/local/include \ 10 | -I ${GOPATH}/src/github.com/envoyproxy/protoc-gen-validate \ 11 | --go_out=plugins=grpc:./pkg/pb/${mod}pb --go_opt=paths=source_relative \ 12 | --validate_out="lang=go,paths=source_relative:./pkg/pb/${mod}pb" \ 13 | proto/${mod}.proto 14 | done 15 | -------------------------------------------------------------------------------- /scripts/test-last-changed-detector.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -uo pipefail 4 | 5 | CHANGED=$(git diff --name-only --no-commit-id origin/main | grep pkg/detectors | grep -v test) 6 | while IFS= read -r FILE; do 7 | DIRECTORY=$(basename $FILE ".go") 8 | if [ -d "pkg/detectors/$DIRECTORY" ] 9 | then 10 | echo $DIRECTORY 11 | go test -v "github.com/trufflesecurity/trufflehog/v3/pkg/detectors/$DIRECTORY" 12 | retVal=$? 13 | if [ $retVal -ne 0 ]; then 14 | exit 1 15 | fi 16 | fi 17 | done <<< "$CHANGED" 18 | -------------------------------------------------------------------------------- /scripts/test_changed_detectors.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -uo pipefail 4 | 5 | CHANGED=$(git diff --name-only --no-commit-id origin/master | grep pkg/detectors | grep -v test) 6 | while IFS= read -r FILE; do 7 | DIRECTORY=$(basename $FILE ".go") 8 | if [ -d "pkg/detectors/$DIRECTORY" ] 9 | then 10 | echo $DIRECTORY 11 | go test -v "github.com/trufflesecurity/trufflehog/v3/pkg/detectors/$DIRECTORY" 12 | retVal=$? 13 | if [ $retVal -ne 0 ]; then 14 | exit 1 15 | fi 16 | fi 17 | done <<< "$CHANGED" 18 | --------------------------------------------------------------------------------