* Add POC analyze sub-command

* Address lint errors

* [chore] Embed scopes at compile time

* [chore] Move subcommand check up to prevent printing metrics

* added http logging to most analyzers

* Use custom RoundTripper with default http.Client

* Create framework of interfaces, structs, and protos

* Merge main

* Add AnalysisInfo to detectors.Result

* Hide analyze subcommand

* Update gen_proto.sh

* Update protos

* Make protos

* Update analyzer data types

* Rename argument to credentialInfo

---------

Co-authored-by: Joe Leon <joe.leon@trufflesec.com>
This commit is contained in:
Miccah 2024-07-25 12:06:05 -07:00 committed by GitHub
parent c4aab3fb51
commit 2424683923
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
47 changed files with 10107 additions and 57 deletions

13
go.mod
View file

@ -35,6 +35,7 @@ require (
github.com/coinbase/waas-client-library-go v1.0.8
github.com/couchbase/gocb/v2 v2.9.1
github.com/crewjam/rfc5424 v0.1.0
github.com/dustin/go-humanize v1.0.1
github.com/elastic/go-elasticsearch/v8 v8.14.0
github.com/envoyproxy/protoc-gen-validate v1.0.4
github.com/fatih/color v1.17.0
@ -53,11 +54,14 @@ require (
github.com/golang-jwt/jwt/v4 v4.5.0
github.com/google/go-cmp v0.6.0
github.com/google/go-containerregistry v0.20.1
github.com/google/go-github/v59 v59.0.0
github.com/google/go-github/v62 v62.0.0
github.com/google/uuid v1.6.0
github.com/googleapis/gax-go/v2 v2.13.0
github.com/hashicorp/go-retryablehttp v0.7.7
github.com/hashicorp/golang-lru/v2 v2.0.7
github.com/jedib0t/go-pretty v4.3.0+incompatible
github.com/jedib0t/go-pretty/v6 v6.5.9
github.com/jlaffaye/ftp v0.2.0
github.com/joho/godotenv v1.5.1
github.com/jpillora/overseer v1.1.6
@ -76,6 +80,7 @@ require (
github.com/prometheus/client_golang v1.19.1
github.com/rabbitmq/amqp091-go v1.10.0
github.com/sassoftware/go-rpmutils v0.4.0
github.com/sendgrid/sendgrid-go v3.14.0+incompatible
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3
github.com/shuheiktgw/go-travis v0.3.1
github.com/snowflakedb/gosnowflake v1.10.1
@ -90,6 +95,7 @@ require (
github.com/trufflesecurity/disk-buffer-reader v0.2.1
github.com/wasilibs/go-re2 v1.6.0
github.com/xanzy/go-gitlab v0.107.0
github.com/xo/dburl v0.23.2
go.mongodb.org/mongo-driver v1.16.0
go.uber.org/mock v0.4.0
go.uber.org/zap v1.27.0
@ -102,6 +108,7 @@ require (
google.golang.org/api v0.189.0
google.golang.org/protobuf v1.34.2
gopkg.in/h2non/gock.v1 v1.1.2
gopkg.in/yaml.v2 v2.4.0
pault.ag/go/debian v0.16.0
pgregory.net/rapid v1.1.0
sigs.k8s.io/yaml v1.4.0
@ -137,6 +144,7 @@ require (
github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137 // indirect
github.com/andybalholm/brotli v1.1.0 // indirect
github.com/apache/arrow/go/v14 v14.0.2 // indirect
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect
github.com/atotto/clipboard v0.1.4 // indirect
github.com/aws/smithy-go v1.20.1 // indirect
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
@ -186,6 +194,8 @@ require (
github.com/go-jose/go-jose/v4 v4.0.2 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-ole/go-ole v1.2.6 // indirect
github.com/go-openapi/errors v0.22.0 // indirect
github.com/go-openapi/strfmt v0.23.0 // indirect
github.com/goccy/go-json v0.10.2 // indirect
github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
@ -224,6 +234,7 @@ require (
github.com/mattn/go-runewidth v0.0.15 // indirect
github.com/microcosm-cc/bluemonday v1.0.25 // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/moby/docker-image-spec v1.3.1 // indirect
github.com/moby/patternmatcher v0.6.0 // indirect
github.com/moby/sys/sequential v0.5.0 // indirect
@ -236,6 +247,7 @@ require (
github.com/muesli/cancelreader v0.2.2 // indirect
github.com/muesli/termenv v0.15.2 // indirect
github.com/nwaples/rardecode/v2 v2.0.0-beta.2 // indirect
github.com/oklog/ulid v1.3.1 // indirect
github.com/olekukonko/tablewriter v0.0.5 // indirect
github.com/onsi/ginkgo v1.16.5 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
@ -250,6 +262,7 @@ require (
github.com/prometheus/procfs v0.12.0 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/sahilm/fuzzy v0.1.1-0.20230530133925-c48e322e2a8f // indirect
github.com/sendgrid/rest v2.6.9+incompatible // indirect
github.com/shirou/gopsutil/v3 v3.23.12 // indirect
github.com/shoenig/go-m1cpu v0.1.6 // indirect
github.com/sirupsen/logrus v1.9.3 // indirect

28
go.sum
View file

@ -119,12 +119,10 @@ github.com/apache/arrow/go/v14 v14.0.2 h1:N8OkaJEOfI3mEZt07BIkvo4sC6XDbL+48MBPWO
github.com/apache/arrow/go/v14 v14.0.2/go.mod h1:u3fgh3EdgN/YQ8cVQRguVW3R+seMybFg8QBQ5LU+eBY=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so=
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw=
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
github.com/aws/aws-sdk-go v1.54.20 h1:FZ2UcXya7bUkvkpf7TaPmiL7EubK0go1nlXGLRwEsoo=
github.com/aws/aws-sdk-go v1.54.20/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU=
github.com/aws/aws-sdk-go v1.55.1 h1:ZTNPmbRMxaK5RlTJrBullX9r/rF1MPf3yAJOLlwDiT8=
github.com/aws/aws-sdk-go v1.55.1/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU=
github.com/aws/aws-sdk-go v1.55.2 h1:/2OFM8uFfK9e+cqHTw9YPrvTzIXT2XkFGXRM7WbJb7E=
github.com/aws/aws-sdk-go v1.55.2/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU=
github.com/aws/smithy-go v1.20.1 h1:4SZlSlMr36UEqC7XOyRVb27XMeZubNcBNN+9IgEPIQw=
@ -243,6 +241,8 @@ github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDD
github.com/dsnet/compress v0.0.1 h1:PlZu0n3Tuv04TzpfPbrnI0HW/YwodEXDS+oPKahKF0Q=
github.com/dsnet/compress v0.0.1/go.mod h1:Aw8dCMJ7RioblQeTqt88akK31OvO8Dhf5JflhBbQEHo=
github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdfkVLjJ8T6VcRQv3SXugXy999NBtR9aFY=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/dvsekhvalnov/jose2go v1.6.0 h1:Y9gnSnP4qEI0+/uQkHvFXeD2PLPJeXEL+ySMEA2EjTY=
github.com/dvsekhvalnov/jose2go v1.6.0/go.mod h1:QsHjhyTlD/lAVqn/NSbVZmSCGeDehTB/mPZadG+mhXU=
github.com/elastic/elastic-transport-go/v8 v8.6.0 h1:Y2S/FBjx1LlCv5m6pWAF2kDJAHoSjSRSJCApolgfthA=
@ -308,6 +308,10 @@ github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ=
github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg=
github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
github.com/go-openapi/errors v0.22.0 h1:c4xY/OLxUBSTiepAg3j/MHuAv5mJhnf53LLMWFB+u/w=
github.com/go-openapi/errors v0.22.0/go.mod h1:J3DmZScxCDufmIMsdOuDHxJbdOGC0xtUynjIx092vXE=
github.com/go-openapi/strfmt v0.23.0 h1:nlUS6BCqcnAk0pyhi9Y+kdDVZdZMHfEKQiS4HaMgO/c=
github.com/go-openapi/strfmt v0.23.0/go.mod h1:NrtIpfKtWIygRkKVsxh7XQMDQW5HKQl6S5ik2elW+K4=
github.com/go-redis/redis v6.15.9+incompatible h1:K0pv1D7EQUjfyoMql+r/jZqCLizCGKFlFgcHWWmHQjg=
github.com/go-redis/redis v6.15.9+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA=
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
@ -384,6 +388,8 @@ github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-containerregistry v0.20.1 h1:eTgx9QNYugV4DN5mz4U8hiAGTi1ybXn0TPi4Smd8du0=
github.com/google/go-containerregistry v0.20.1/go.mod h1:YCMFNQeeXeLF+dnhhWkqDItx/JSkH01j1Kis4PsjzFI=
github.com/google/go-github/v59 v59.0.0 h1:7h6bgpF5as0YQLLkEiVqpgtJqjimMYhBkD4jT5aN3VA=
github.com/google/go-github/v59 v59.0.0/go.mod h1:rJU4R0rQHFVFDOkqGWxfLNo6vEk4dv40oDjhV/gH6wM=
github.com/google/go-github/v62 v62.0.0 h1:/6mGCaRywZz9MuHyw9gD1CwsbmBX8GWsbFkwMmHdhl4=
github.com/google/go-github/v62 v62.0.0/go.mod h1:EMxeUqGJq2xRu9DYBMwel/mr7kZrzUOfQmmpYrZn2a4=
github.com/google/go-querystring v0.0.0-20170111101155-53e6ce116135/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
@ -468,6 +474,10 @@ github.com/jcmturner/gokrb5/v8 v8.4.4 h1:x1Sv4HaTpepFkXbt2IkL29DXRf8sOfZXo8eRKh6
github.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs=
github.com/jcmturner/rpc/v2 v2.0.3 h1:7FXXj8Ti1IaVFpSAziCZWNzbNuZmnvw/i6CqLNdWfZY=
github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc=
github.com/jedib0t/go-pretty v4.3.0+incompatible h1:CGs8AVhEKg/n9YbUenWmNStRW2PHJzaeDodcfvRAbIo=
github.com/jedib0t/go-pretty v4.3.0+incompatible/go.mod h1:XemHduiw8R651AF9Pt4FwCTKeG3oo7hrHJAoznj9nag=
github.com/jedib0t/go-pretty/v6 v6.5.9 h1:ACteMBRrrmm1gMsXe9PSTOClQ63IXDUt03H5U+UV8OU=
github.com/jedib0t/go-pretty/v6 v6.5.9/go.mod h1:zbn98qrYlh95FIhwwsbIip0LYpwSG8SUOScs+v9/t0E=
github.com/jlaffaye/ftp v0.2.0 h1:lXNvW7cBu7R/68bknOX3MrRIIqZ61zELs1P2RAiA3lg=
github.com/jlaffaye/ftp v0.2.0/go.mod h1:is2Ds5qkhceAPy2xD6RLI6hmp/qysSoymZ+Z2uTnspI=
github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
@ -542,6 +552,8 @@ github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/go-ps v1.0.0 h1:i6ampVEEF4wQFF+bkYfwYgY+F/uYJDktmvLPf7qIgjc=
github.com/mitchellh/go-ps v1.0.0/go.mod h1:J4lOc8z8yJs6vUwklHw2XEIiT4z4C40KtWVN3nvg8Pg=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk=
@ -575,6 +587,8 @@ github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
github.com/nxadm/tail v1.4.11 h1:8feyoE3OzPrcshW5/MJ4sGESc5cqmGkGCWlco4l0bqY=
github.com/nxadm/tail v1.4.11/go.mod h1:OTaG3NK980DZzxbRq6lEuzgU+mug70nY11sMd4JXXHc=
github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4=
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
@ -637,6 +651,10 @@ github.com/sahilm/fuzzy v0.1.1-0.20230530133925-c48e322e2a8f h1:MvTmaQdww/z0Q4wr
github.com/sahilm/fuzzy v0.1.1-0.20230530133925-c48e322e2a8f/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y=
github.com/sassoftware/go-rpmutils v0.4.0 h1:ojND82NYBxgwrV+mX1CWsd5QJvvEZTKddtCdFLPWhpg=
github.com/sassoftware/go-rpmutils v0.4.0/go.mod h1:3goNWi7PGAT3/dlql2lv3+MSN5jNYPjT5mVcQcIsYzI=
github.com/sendgrid/rest v2.6.9+incompatible h1:1EyIcsNdn9KIisLW50MKwmSRSK+ekueiEMJ7NEoxJo0=
github.com/sendgrid/rest v2.6.9+incompatible/go.mod h1:kXX7q3jZtJXK5c5qK83bSGMdV6tsOE70KbHoqJls4lE=
github.com/sendgrid/sendgrid-go v3.14.0+incompatible h1:KDSasSTktAqMJCYClHVE94Fcif2i7P7wzISv1sU6DUA=
github.com/sendgrid/sendgrid-go v3.14.0+incompatible/go.mod h1:QRQt+LX/NmgVEvmdRw0VT/QgUn499+iza2FnDca9fg8=
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8=
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
@ -734,6 +752,8 @@ github.com/xhit/go-str2duration/v2 v2.1.0 h1:lxklc02Drh6ynqX+DdPyp5pCKLUQpRT8bp8
github.com/xhit/go-str2duration/v2 v2.1.0/go.mod h1:ohY8p+0f07DiV6Em5LKB0s2YpLtXVyJfNt1+BlmyAsU=
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 h1:nIPpBwaJSVYIxUFsDv3M8ofmx9yWTog9BfvIu0q41lo=
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8/go.mod h1:HUYIGzjTL3rfEspMxjDjgmT5uz5wzYJKVo23qUhYTos=
github.com/xo/dburl v0.23.2 h1:Fl88cvayrgE56JA/sqhNMLljCW/b7RmG1mMkKMZUFgA=
github.com/xo/dburl v0.23.2/go.mod h1:uazlaAQxj4gkshhfuuYyvwCBouOmNnG2aDxTCFZpmL4=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d h1:splanxYIlg+5LfHAM6xpdFEAYOk8iySO56hMFq6uLyA=

93
main.go
View file

@ -21,6 +21,7 @@ import (
"github.com/jpillora/overseer"
"github.com/mattn/go-isatty"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer"
"github.com/trufflesecurity/trufflehog/v3/pkg/cleantemp"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/config"
@ -35,8 +36,57 @@ import (
"github.com/trufflesecurity/trufflehog/v3/pkg/version"
)
// usageTemplate is a copy of kingpin.DefaultUsageTemplate with a minor change
// to not list all flattened commands. This is required to hide all of the
// analyze sub-commands from the main help.
const usageTemplate = `{{define "FormatCommand" -}}
{{if .FlagSummary}} {{.FlagSummary}}{{end -}}
{{range .Args}}{{if not .Hidden}} {{if not .Required}}[{{end}}{{if .PlaceHolder}}{{.PlaceHolder}}{{else}}<{{.Name}}>{{end}}{{if .Value|IsCumulative}}...{{end}}{{if not .Required}}]{{end}}{{end}}{{end -}}
{{end -}}
{{define "FormatCommands" -}}
{{range .Commands -}}
{{if not .Hidden -}}
{{.FullCommand}}{{if .Default}}*{{end}}{{template "FormatCommand" .}}
{{.Help|Wrap 4}}
{{end -}}
{{end -}}
{{end -}}
{{define "FormatUsage" -}}
{{template "FormatCommand" .}}{{if .Commands}} <command> [<args> ...]{{end}}
{{if .Help}}
{{.Help|Wrap 0 -}}
{{end -}}
{{end -}}
{{if .Context.SelectedCommand -}}
usage: {{.App.Name}} {{.Context.SelectedCommand}}{{template "FormatUsage" .Context.SelectedCommand}}
{{ else -}}
usage: {{.App.Name}}{{template "FormatUsage" .App}}
{{end}}
{{if .Context.Flags -}}
Flags:
{{.Context.Flags|FlagsToTwoColumns|FormatTwoColumns}}
{{end -}}
{{if .Context.Args -}}
Args:
{{.Context.Args|ArgsToTwoColumns|FormatTwoColumns}}
{{end -}}
{{if .Context.SelectedCommand -}}
{{if len .Context.SelectedCommand.Commands -}}
Subcommands:
{{template "FormatCommands" .Context.SelectedCommand}}
{{end -}}
{{else if .App.Commands -}}
Commands:
{{template "FormatCommands" .App}}
{{end -}}
`
var (
cli = kingpin.New("TruffleHog", "TruffleHog is a tool for finding credentials.")
cli = kingpin.New("TruffleHog", "TruffleHog is a tool for finding credentials.").UsageTemplate(usageTemplate)
cmd string
debug = cli.Flag("debug", "Run in debug mode.").Bool()
trace = cli.Flag("trace", "Run in trace mode.").Bool()
@ -219,7 +269,8 @@ var (
huggingfaceIncludeDiscussions = huggingfaceScan.Flag("include-discussions", "Include discussions in scan.").Bool()
huggingfaceIncludePrs = huggingfaceScan.Flag("include-prs", "Include pull requests in scan.").Bool()
usingTUI = false
analyzeCmd = analyzer.Command(cli)
usingTUI = false
)
func init() {
@ -423,24 +474,30 @@ func run(state overseer.State) {
return
}
metrics, err := runSingleScan(ctx, cmd, engConf)
if err != nil {
logFatal(err, "error running scan")
}
topLevelSubCommand, _, _ := strings.Cut(cmd, " ")
switch topLevelSubCommand {
case analyzeCmd.FullCommand():
analyzer.Run(cmd)
default:
metrics, err := runSingleScan(ctx, cmd, engConf)
if err != nil {
logFatal(err, "error running scan")
}
// Print results.
logger.Info("finished scanning",
"chunks", metrics.ChunksScanned,
"bytes", metrics.BytesScanned,
"verified_secrets", metrics.VerifiedSecretsFound,
"unverified_secrets", metrics.UnverifiedSecretsFound,
"scan_duration", metrics.ScanDuration.String(),
"trufflehog_version", version.BuildVersion,
)
// Print results.
logger.Info("finished scanning",
"chunks", metrics.ChunksScanned,
"bytes", metrics.BytesScanned,
"verified_secrets", metrics.VerifiedSecretsFound,
"unverified_secrets", metrics.UnverifiedSecretsFound,
"scan_duration", metrics.ScanDuration.String(),
"trufflehog_version", version.BuildVersion,
)
if metrics.hasFoundResults && *fail {
logger.V(2).Info("exiting with code 183 because results were found")
os.Exit(183)
if metrics.hasFoundResults && *fail {
logger.V(2).Info("exiting with code 183 because results were found")
os.Exit(183)
}
}
}

View file

@ -0,0 +1,127 @@
package airbrake
import (
"encoding/json"
"fmt"
"net/http"
"os"
"strconv"
"github.com/fatih/color"
"github.com/jedib0t/go-pretty/table"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/config"
)
type ProjectsJSON struct {
Projects []struct {
Name string `json:"name"`
ID int `json:"id"`
} `json:"projects"`
}
// validateKey checks if the key is valid and returns the projects associated with the key
func validateKey(cfg *config.Config, key string) (bool, ProjectsJSON, error) {
// create struct to hold response
var projects ProjectsJSON
// create http client
client := analyzers.NewAnalyzeClient(cfg)
// create request
req, err := http.NewRequest("GET", "https://api.airbrake.io/api/v4/projects", nil)
if err != nil {
return false, projects, err
}
// add key as url param
q := req.URL.Query()
q.Add("key", key)
req.URL.RawQuery = q.Encode()
// send request
resp, err := client.Do(req)
if err != nil {
return false, projects, err
}
// read response
defer resp.Body.Close()
// if status code is 200, decode response
if resp.StatusCode == 200 {
err := json.NewDecoder(resp.Body).Decode(&projects)
return true, projects, err
}
// if status code is not 200, return false
return false, projects, nil
}
func AnalyzePermissions(cfg *config.Config, key string) {
// validate key
valid, projects, err := validateKey(cfg, key)
if err != nil {
color.Red("[x]" + err.Error())
return
}
if !valid {
color.Red("[x] Invalid Airbrake User API Key")
return
}
color.Green("[!] Valid Airbrake User API Key\n\n")
if len(key) == 40 {
color.Green("[i] Key Type: User Key")
color.Green("[i] Expiration: Never")
} else {
color.Yellow("[i] Key Type: User Token")
color.Yellow("[i] Duration: Short-Lived")
// ToDo: determine how long these are valid for
}
// if key is valid, print projects
if valid {
color.Green("\n[i] Projects:")
printProjects(projects)
}
color.Green("\n[i] Permissions:")
printPermissions()
}
func printProjects(projects ProjectsJSON) {
t := table.NewWriter()
t.SetOutputMirror(os.Stdout)
t.AppendHeader(table.Row{"Project ID", "Project Name"})
for _, project := range projects.Projects {
t.AppendRow([]interface{}{color.GreenString(strconv.Itoa(project.ID)), color.GreenString(project.Name)})
}
t.Render()
}
func printPermissions() {
t := table.NewWriter()
t.SetOutputMirror(os.Stdout)
t.AppendHeader(table.Row{"Scope", "Permissions"})
for s := range scope_order {
scope := scope_order[s][0]
permissions := scope_mapping[scope]
if scope == "Authentication" {
t.AppendRow([]interface{}{scope, permissions[0]})
continue
}
for i, permission := range permissions {
if i == 0 {
t.AppendRow([]interface{}{color.GreenString(scope), color.GreenString(permission)})
} else {
t.AppendRow([]interface{}{"", color.GreenString(permission)})
}
}
}
t.Render()
fmt.Println("| Ref: https://docs.airbrake.io/docs/devops-tools/api/ |")
fmt.Println("+------------------------+---------------------------------+")
}

View file

@ -0,0 +1,27 @@
package airbrake
var scope_order = [][]string{
{"Authentication"},
{"Performance Monitoring"},
{"Error Notification"},
{"Projects"},
{"Deploys"},
{"Groups"},
{"Notices"},
{"Project Activities"},
{"Source Maps"},
{"iOS Crash Reports"},
}
var scope_mapping = map[string][]string{
"Authentication": {"Create user token"},
"Performance Monitoring": {"Route performance endpoint", "Routes breakdown endpoint", "Database query stats", "Queue stats"},
"Error Notification": {"Create notice"},
"Projects": {"List projects", "Show projects"},
"Deploys": {"Create deploy", "List deploys", "Show deploy"},
"Groups": {"List groups", "Show group", "Mute group", "Unmute group", "Delete group", "List groups across all projects", "Show group statistics"},
"Notices": {"List notices", "Show notice status"},
"Project Activities": {"List project activities", "Show project statistics"},
"Source Maps": {"Create source map", "List source maps", "Show source map", "Delete source map"},
"iOS Crash Reports": {"Create iOS crash report"},
}

View file

@ -0,0 +1,217 @@
package analyzers
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"time"
"github.com/fatih/color"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/config"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/pb/analyzerpb"
"github.com/trufflesecurity/trufflehog/v3/pkg/context"
)
type (
Analyzer interface {
Type() analyzerpb.AnalyzerType
Analyze(ctx context.Context, credentialInfo map[string]string) (*AnalyzerResult, error)
}
// AnalyzerResult is the output of analysis.
AnalyzerResult struct {
AnalyzerType analyzerpb.AnalyzerType
Bindings []Binding
UnboundedResources []Resource
Metadata map[string]any
}
Resource struct {
Name string
FullyQualifiedName string
Type string
Metadata map[string]any
Parent *Resource
}
Permission struct {
Value string
AccessLevel string
Parent *Permission
}
Binding struct {
Resource Resource
Permission Permission
}
)
type PermissionType string
const (
READ PermissionType = "Read"
WRITE PermissionType = "Write"
READ_WRITE PermissionType = "Read & Write"
NONE PermissionType = "None"
ERROR PermissionType = "Error"
)
type PermissionStatus struct {
Value bool
IsError bool
}
type HttpStatusTest struct {
URL string
Method string
Payload map[string]interface{}
Params map[string]string
Valid []int
Invalid []int
Type PermissionType
Status PermissionStatus
Risk string
}
func (h *HttpStatusTest) RunTest(headers map[string]string) error {
// If body data, marshal to JSON
var data io.Reader
if h.Payload != nil {
jsonData, err := json.Marshal(h.Payload)
if err != nil {
return err
}
data = bytes.NewBuffer(jsonData)
}
// Create new HTTP request
client := &http.Client{}
req, err := http.NewRequest(h.Method, h.URL, data)
if err != nil {
return err
}
// Add custom headers if provided
for key, value := range headers {
req.Header.Set(key, value)
}
// Execute HTTP Request
resp, err := client.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
// Check response status code
switch {
case StatusContains(resp.StatusCode, h.Valid):
h.Status.Value = true
case StatusContains(resp.StatusCode, h.Invalid):
h.Status.Value = false
default:
h.Status.IsError = true
}
return nil
}
type Scope struct {
Name string
Tests []interface{}
}
func StatusContains(status int, vals []int) bool {
for _, v := range vals {
if status == v {
return true
}
}
return false
}
func GetWriterFromStatus(status PermissionType) func(a ...interface{}) string {
switch status {
case READ:
return color.New(color.FgYellow).SprintFunc()
case WRITE:
return color.New(color.FgGreen).SprintFunc()
case READ_WRITE:
return color.New(color.FgGreen).SprintFunc()
case NONE:
return color.New().SprintFunc()
case ERROR:
return color.New(color.FgRed).SprintFunc()
default:
return color.New().SprintFunc()
}
}
var GreenWriter = color.New(color.FgGreen).SprintFunc()
var YellowWriter = color.New(color.FgYellow).SprintFunc()
var RedWriter = color.New(color.FgRed).SprintFunc()
var DefaultWriter = color.New().SprintFunc()
type AnalyzeClient struct {
http.Client
LoggingEnabled bool
LogFile string
}
func CreateLogFileName(baseName string) string {
// Get the current time
currentTime := time.Now()
// Format the time as "2024_06_30_07_15_30"
timeString := currentTime.Format("2006_01_02_15_04_05")
// Create the log file name
logFileName := fmt.Sprintf("%s_%s.log", timeString, baseName)
return logFileName
}
func NewAnalyzeClient(cfg *config.Config) *http.Client {
if cfg == nil || !cfg.LoggingEnabled {
return &http.Client{}
}
return &http.Client{
Transport: LoggingRoundTripper{
parent: http.DefaultTransport,
logFile: cfg.LogFile,
},
}
}
type LoggingRoundTripper struct {
parent http.RoundTripper
// TODO: io.Writer
logFile string
}
func (r LoggingRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
startTime := time.Now()
resp, err := r.parent.RoundTrip(req)
if err != nil {
return resp, err
}
// TODO: JSON
logEntry := fmt.Sprintf("Date: %s, Method: %s, Path: %s, Status: %d\n", startTime.Format(time.RFC3339), req.Method, req.URL.Path, resp.StatusCode)
// Open log file in append mode.
file, err := os.OpenFile(r.logFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
return resp, fmt.Errorf("failed to open log file: %w", err)
}
defer file.Close()
// Write log entry to file.
if _, err := file.WriteString(logEntry); err != nil {
return resp, fmt.Errorf("failed to write log entry to file: %w", err)
}
return resp, nil
}

View file

@ -0,0 +1,85 @@
package asana
// ToDo: Add OAuth token support.
import (
"encoding/json"
"net/http"
"os"
"github.com/fatih/color"
"github.com/jedib0t/go-pretty/table"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/config"
)
type MeJSON struct {
Data struct {
Email string `json:"email"`
Name string `json:"name"`
Type string `json:"resource_type"`
Workspaces []struct {
Name string `json:"name"`
} `json:"workspaces"`
} `json:"data"`
}
func getMetadata(cfg *config.Config, key string) (MeJSON, error) {
var me MeJSON
client := analyzers.NewAnalyzeClient(cfg)
req, err := http.NewRequest("GET", "https://app.asana.com/api/1.0/users/me", nil)
if err != nil {
return me, err
}
req.Header.Set("Authorization", "Bearer "+key)
resp, err := client.Do(req)
if err != nil {
return me, err
}
if resp.StatusCode != 200 {
return me, nil
}
defer resp.Body.Close()
err = json.NewDecoder(resp.Body).Decode(&me)
if err != nil {
return me, err
}
return me, nil
}
func AnalyzePermissions(cfg *config.Config, key string) {
me, err := getMetadata(cfg, key)
if err != nil {
color.Red("[x] ", err.Error())
return
}
printMetadata(me)
}
func printMetadata(me MeJSON) {
if me.Data.Email == "" {
color.Red("[x] Invalid Asana API Key\n")
return
}
color.Green("[!] Valid Asana API Key\n\n")
color.Yellow("[i] User Information")
color.Yellow(" Name: %s", me.Data.Name)
color.Yellow(" Email: %s", me.Data.Email)
color.Yellow(" Type: %s\n\n", me.Data.Type)
color.Green("[i] Permissions: Full Access\n\n")
color.Yellow("[i] Accessible Workspaces")
t := table.NewWriter()
t.SetOutputMirror(os.Stdout)
t.AppendHeader(table.Row{"Workspace Name"})
for _, workspace := range me.Data.Workspaces {
t.AppendRow(table.Row{color.GreenString(workspace.Name)})
}
t.Render()
}

View file

@ -0,0 +1,196 @@
package bitbucket
import (
"encoding/json"
"net/http"
"os"
"sort"
"strings"
"github.com/fatih/color"
"github.com/jedib0t/go-pretty/table"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/config"
)
type Repo struct {
FullName string `json:"full_name"`
RepoName string `json:"name"`
Project struct {
Name string `json:"name"`
} `json:"project"`
Workspace struct {
Name string `json:"name"`
} `json:"workspace"`
IsPrivate bool `json:"is_private"`
Owner struct {
Username string `json:"username"`
} `json:"owner"`
Role string
}
type RepoJSON struct {
Values []Repo `json:"values"`
}
func getScopesAndType(cfg *config.Config, key string) (string, string, error) {
// client
client := analyzers.NewAnalyzeClient(cfg)
// request
req, err := http.NewRequest("GET", "https://api.bitbucket.org/2.0/repositories", nil)
if err != nil {
return "", "", err
}
// headers
req.Header.Set("Authorization", "Bearer "+key)
// response
resp, err := client.Do(req)
if err != nil {
return "", "", err
}
defer resp.Body.Close()
// parse response headers
credentialType := resp.Header.Get("x-credential-type")
oauthScopes := resp.Header.Get("x-oauth-scopes")
return credentialType, oauthScopes, nil
}
func getRepositories(cfg *config.Config, key string, role string) (RepoJSON, error) {
var repos RepoJSON
// client
client := analyzers.NewAnalyzeClient(cfg)
// request
req, err := http.NewRequest("GET", "https://api.bitbucket.org/2.0/repositories", nil)
if err != nil {
return repos, err
}
// headers
req.Header.Set("Authorization", "Bearer "+key)
// add query params
q := req.URL.Query()
q.Add("role", role)
q.Add("pagelen", "100")
req.URL.RawQuery = q.Encode()
// response
resp, err := client.Do(req)
if err != nil {
return repos, err
}
defer resp.Body.Close()
// parse response body
err = json.NewDecoder(resp.Body).Decode(&repos)
if err != nil {
return repos, err
}
return repos, nil
}
func getAllRepos(cfg *config.Config, key string) (map[string]Repo, error) {
roles := []string{"member", "contributor", "admin", "owner"}
var allRepos = make(map[string]Repo, 0)
for _, role := range roles {
repos, err := getRepositories(cfg, key, role)
if err != nil {
return allRepos, err
}
// purposefully overwriting, so that get the most permissive role
for _, repo := range repos.Values {
repo.Role = role
allRepos[repo.FullName] = repo
}
}
return allRepos, nil
}
func AnalyzePermissions(cfg *config.Config, key string) {
credentialType, oauthScopes, err := getScopesAndType(cfg, key)
if err != nil {
color.Red("Error: %s", err)
return
}
printScopes(credentialType, oauthScopes)
// get all repos available to user
// ToDo: pagination
repos, err := getAllRepos(cfg, key)
if err != nil {
color.Red("Error: %s", err)
return
}
printAccessibleRepositories(repos)
}
func printScopes(credentialType string, oauthScopes string) {
if credentialType == "" {
color.Red("[x] Invalid Bitbucket access token.")
return
}
color.Green("[!] Valid Bitbucket access token.\n\n")
color.Green("[i] Credential Type: %s\n\n", credential_type_map[credentialType])
scopes := strings.Split(oauthScopes, ", ")
scopesSlice := []BitbucketScope{}
for _, scope := range scopes {
mapping := oauth_scope_map[scope]
for _, impliedScope := range mapping.ImpliedScopes {
scopesSlice = append(scopesSlice, oauth_scope_map[impliedScope])
}
scopesSlice = append(scopesSlice, oauth_scope_map[scope])
}
// sort scopes by category
sort.Sort(ByCategoryAndName(scopesSlice))
color.Yellow("[i] Access Token Scopes:")
t := table.NewWriter()
t.SetOutputMirror(os.Stdout)
t.AppendHeader(table.Row{"Category", "Permission"})
currentCategory := ""
for _, scope := range scopesSlice {
if currentCategory != scope.Category {
currentCategory = scope.Category
t.AppendRow([]interface{}{scope.Category, ""})
}
t.AppendRow([]interface{}{"", color.GreenString(scope.Name)})
}
t.Render()
}
func printAccessibleRepositories(repos map[string]Repo) {
color.Yellow("\n[i] Accessible Repositories:")
t := table.NewWriter()
t.SetOutputMirror(os.Stdout)
t.AppendHeader(table.Row{"Repository", "Project", "Workspace", "Owner", "Is Private", "This User's Role"})
for _, repo := range repos {
private := ""
if repo.IsPrivate {
private = color.GreenString("Yes")
} else {
private = color.RedString("No")
}
t.AppendRow([]interface{}{color.GreenString(repo.RepoName), color.GreenString(repo.Project.Name), color.GreenString(repo.Workspace.Name), color.GreenString(repo.Owner.Username), private, color.GreenString(repo.Role)})
}
t.Render()
}

View file

@ -0,0 +1,112 @@
package bitbucket
var credential_type_map = map[string]string{
"repo_access_token": "Repository Access Token (Can access 1 repository)",
"project_access_token": "Project Access Token (Can access all repos in 1 project)",
"workspace_access_token": "Workspace Access Token (Can access all projects and repos in 1 workspace)",
}
type BitbucketScope struct {
Name string `json:"name"`
Category string `json:"category"`
ImpliedScopes []string `json:"implied_scopes"`
}
type ByCategoryAndName []BitbucketScope
func (a ByCategoryAndName) Len() int { return len(a) }
func (a ByCategoryAndName) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func (a ByCategoryAndName) Less(i, j int) bool {
categoryOrder := map[string]int{
"Account": 0,
"Projects": 1,
"Repositories": 2,
"Pull Requests": 3,
"Webhooks": 4,
"Pipelines": 5,
"Runners": 6,
}
nameOrder := map[string]int{
"Read": 0,
"Write": 1,
"Admin": 2,
"Delete": 3,
"Edit variables": 4,
"Read and write": 5,
}
if categoryOrder[a[i].Category] != categoryOrder[a[j].Category] {
return categoryOrder[a[i].Category] < categoryOrder[a[j].Category]
}
return nameOrder[a[i].Name] < nameOrder[a[j].Name]
}
var oauth_scope_map = map[string]BitbucketScope{
"repository": {
Name: "Read",
Category: "Repositories",
},
"repository:write": {
Name: "Write",
Category: "Repositories",
ImpliedScopes: []string{"repository"},
},
"repository:admin": {
Name: "Admin",
Category: "Repositories",
},
"repository:delete": {
Name: "Delete",
Category: "Repositories",
},
"pullrequest": {
Name: "Read",
Category: "Pull Requests",
ImpliedScopes: []string{"repository"},
},
"pullrequest:write": {
Name: "Write",
Category: "Pull Requests",
ImpliedScopes: []string{"pullrequest", "repository", "repository:write"},
},
"webhook": {
Name: "Read and write",
Category: "Webhooks",
},
"pipeline": {
Name: "Read",
Category: "Pipelines",
},
"pipeline:write": {
Name: "Write",
Category: "Pipelines",
ImpliedScopes: []string{"pipeline"},
},
"pipeline:variable": {
Name: "Edit variables",
Category: "Pipelines",
ImpliedScopes: []string{"pipeline", "pipeline:write"},
},
"runner": {
Name: "Read",
Category: "Runners",
},
"runner:write": {
Name: "Write",
Category: "Runners",
ImpliedScopes: []string{"runner"},
},
"project": {
Name: "Read",
Category: "Projects",
ImpliedScopes: []string{"repository"},
},
"project:admin": {
Name: "Admin",
Category: "Projects",
},
"account": {
Name: "Read",
Category: "Account",
},
}

View file

@ -0,0 +1,202 @@
package github
import (
"context"
"fmt"
"os"
"slices"
"strings"
"github.com/fatih/color"
gh "github.com/google/go-github/v59/github"
"github.com/jedib0t/go-pretty/v6/table"
)
// var SCOPE_ORDER = []string{"repo", "repo:status", "repo_deployment", "public_repo", "repo:invite", "security_events", "--", "workflow", "--", "write:packages", "read:packages", "--", "delete:packages", "--", "admin:org", "write:org", "read:org", "manage_runners:org", "--", "admin:public_key", "write:public_key", "read:public_key", "--", "admin:repo_hook", "write:repo_hook", "read:repo_hook", "--", "admin:org_hook", "--", "gist", "--", "notifications", "--", "user", "read:user", "user:email", "user:follow", "--", "delete_repo", "--", "write:discussion", "read:discussion", "--", "admin:enterprise", "manage_runners:enterprise", "manage_billing:enterprise", "read:enterprise", "--", "audit_log", "read:audit_log", "--", "codespace", "codespace:secrets", "--", "copilot", "manage_billing:copilot", "--", "project", "read:project", "--", "admin:gpg_key", "write:gpg_key", "read:gpg_key", "--", "admin:ssh_signing_key", "write:ssh_signing_key", "read:ssh_signing_key"}
var SCOPE_ORDER = [][]string{{"repo", "repo:status", "repo_deployment", "public_repo", "repo:invite", "security_events"}, {"workflow"}, {"write:packages", "read:packages"}, {"delete:packages"}, {"admin:org", "write:org", "read:org", "manage_runners:org"}, {"admin:public_key", "write:public_key", "read:public_key"}, {"admin:repo_hook", "write:repo_hook", "read:repo_hook"}, {"admin:org_hook"}, {"gist"}, {"notifications"}, {"user", "read:user", "user:email", "user:follow"}, {"delete_repo"}, {"write:discussion", "read:discussion"}, {"admin:enterprise", "manage_runners:enterprise", "manage_billing:enterprise", "read:enterprise"}, {"audit_log", "read:audit_log"}, {"codespace", "codespace:secrets"}, {"copilot", "manage_billing:copilot"}, {"project", "read:project"}, {"admin:gpg_key", "write:gpg_key", "read:gpg_key"}, {"admin:ssh_signing_key", "write:ssh_signing_key", "read:ssh_signing_key"}}
var SCOPE_TO_SUB_SCOPE = map[string][]string{
"repo": {"repo:status", "repo_deployment", "public_repo", "repo:invite", "security_events"},
"write:pakages": {"read:packages"},
"admin:org": {"write:org", "read:org", "manage_runners:org"},
"write:org": {"read:org"},
"admin:public_key": {"write:public_key", "read:public_key"},
"write:public_key": {"read:public_key"},
"admin:repo_hook": {"write:repo_hook", "read:repo_hook"},
"write:repo_hook": {"read:repo_hook"},
"user": {"read:user", "user:email", "user:follow"},
"write:discussion": {"read:discussion"},
"admin:enterprise": {"manage_runners:enterprise", "manage_billing:enterprise", "read:enterprise"},
"manage_billing:enterprise": {"read:enterprise"},
"audit_log": {"read:audit_log"},
"codespace": {"codespace:secrets"},
"copilot": {"manage_billing:copilot"},
"project": {"read:project"},
"admin:gpg_key": {"write:gpg_key", "read:gpg_key"},
"write:gpg_key": {"read:gpg_key"},
"admin:ssh_signing_key": {"write:ssh_signing_key", "read:ssh_signing_key"},
"write:ssh_signing_key": {"read:ssh_signing_key"},
}
func checkPrivateRepoAccess(scopes map[string]bool) []string {
var currPrivateScopes []string
privateScopes := []string{"repo", "repo:status", "repo_deployment", "repo:invite", "security_events", "admin:repo_hook", "write:repo_hook", "read:repo_hook"}
for _, scope := range privateScopes {
if scopes[scope] {
currPrivateScopes = append(currPrivateScopes, scope)
}
}
return currPrivateScopes
}
func processScopes(headerScopesSlice []string) map[string]bool {
allScopes := make(map[string]bool)
for _, scope := range headerScopesSlice {
allScopes[scope] = true
}
for scope := range allScopes {
if subScopes, ok := SCOPE_TO_SUB_SCOPE[scope]; ok {
for _, subScope := range subScopes {
allScopes[subScope] = true
}
}
}
return allScopes
}
// The `gists` scope is required to update private gists. Anyone can access a private gist with the link.
// These tokens can seem to list out the private repos, but access will depend on scopes.
func analyzeClassicToken(client *gh.Client, _ string, show_all bool) {
// Issue GET request to /user
user, resp, err := client.Users.Get(context.Background(), "")
if err != nil {
color.Red("[x] Invalid GitHub Token.")
return
}
// If resp.Header "X-OAuth-Scopes", parse the scopes into a map[string]bool
headerScopes := resp.Header.Get("X-OAuth-Scopes")
var scopes = make(map[string]bool)
if headerScopes == "" {
color.Red("[x] Classic Token has no scopes.")
} else {
// Split string into slice of strings
headerScopesSlice := strings.Split(headerScopes, ", ")
scopes = processScopes(headerScopesSlice)
}
printClassicGHPermissions(scopes, show_all)
// Check if private repo access
privateScopes := checkPrivateRepoAccess(scopes)
if len(privateScopes) > 0 && slices.Contains(privateScopes, "repo") {
color.Green("[!] Token has scope(s) for both public and private repositories. Here's a list of all accessible repositories:")
repos, _ := getAllReposForUser(client)
printGitHubRepos(repos)
} else if len(privateScopes) > 0 {
color.Yellow("[!] Token has scope(s) useful for accessing both public and private repositories.\n However, without the `repo` scope, we cannot enumerate or access code from private repos.\n Review the permissions associated with the following scopes for more details: %v", strings.Join(privateScopes, ", "))
} else if scopes["public_repo"] {
color.Yellow("[i] Token is scoped to only public repositories. See https://github.com/%v?tab=repositories", *user.Login)
} else {
color.Red("[x] Token does not appear scoped to any specific repositories.")
}
// Get all private gists
gists, _ := getAllGistsForUser(client)
printGists(gists, show_all)
}
// Question: can you access private repo with those other permissions? or can we just not list them?
func scopeFormatter(scope string, checked bool, indentation int) (string, string) {
if indentation != 0 {
scope = strings.Repeat(" ", indentation) + scope
}
if checked {
return color.GreenString(scope), color.GreenString("true")
} else {
return scope, "false"
}
}
func printClassicGHPermissions(scopes map[string]bool, show_all bool) {
scopeCount := 0
t := table.NewWriter()
t.SetOutputMirror(os.Stdout)
t.AppendHeader(table.Row{"Scope", "In-Scope" /* Add more column headers if needed */})
filteredScopes := make([][]string, 0)
for _, scopeSlice := range SCOPE_ORDER {
for _, scope := range scopeSlice {
if scopes[scope] {
filteredScopes = append(filteredScopes, scopeSlice)
break
}
}
}
// For ease of reading, divide the scopes into sections, just like the GH UI
var formattedScope, status string
var indentation int
if !show_all {
for _, scopeSlice := range filteredScopes {
for ind, scope := range scopeSlice {
if ind == 0 {
indentation = 0
if scopes[scope] {
scopeCount++
formattedScope, status = scopeFormatter(scope, true, indentation)
t.AppendRow([]interface{}{formattedScope, status})
} else {
t.AppendRow([]interface{}{scope, "----"})
}
} else {
indentation = 2
if scopes[scope] {
scopeCount++
formattedScope, status = scopeFormatter(scope, true, indentation)
t.AppendRow([]interface{}{formattedScope, status})
}
}
}
t.AppendSeparator()
}
} else {
for _, scopeSlice := range SCOPE_ORDER {
for ind, scope := range scopeSlice {
if ind == 0 {
indentation = 0
} else {
indentation = 2
}
if scopes[scope] {
scopeCount++
formattedScope, status = scopeFormatter(scope, true, indentation)
t.AppendRow([]interface{}{formattedScope, status})
} else {
formattedScope, status = scopeFormatter(scope, false, indentation)
t.AppendRow([]interface{}{formattedScope, status})
}
}
t.AppendSeparator()
}
}
if scopeCount == 0 && !show_all {
color.Red("No Scopes Found for the GitHub Token above\n\n")
return
} else if scopeCount == 0 {
color.Red("Found No Scopes for the GitHub Token above\n")
} else {
color.Green(fmt.Sprintf("[!] Found %v Scope(s) for the GitHub Token above\n", scopeCount))
}
t.Render()
fmt.Print("\n\n")
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,204 @@
package github
import (
"context"
"fmt"
"os"
"strings"
"time"
"github.com/fatih/color"
gh "github.com/google/go-github/v59/github"
"github.com/jedib0t/go-pretty/v6/table"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/config"
)
func getAllGistsForUser(client *gh.Client) ([]*gh.Gist, error) {
opt := &gh.GistListOptions{ListOptions: gh.ListOptions{PerPage: 100}}
var allGists []*gh.Gist
page := 1
for {
opt.Page = page
gists, resp, err := client.Gists.List(context.Background(), "", opt)
if err != nil {
color.Red("Error getting gists.")
return nil, err
}
allGists = append(allGists, gists...)
linkHeader := resp.Header.Get("link")
if linkHeader == "" || !strings.Contains(linkHeader, `rel="next"`) {
break
}
page++
}
return allGists, nil
}
func getAllReposForUser(client *gh.Client) ([]*gh.Repository, error) {
opt := &gh.RepositoryListByAuthenticatedUserOptions{ListOptions: gh.ListOptions{PerPage: 100}}
var allRepos []*gh.Repository
page := 1
for {
opt.Page = page
repos, resp, err := client.Repositories.ListByAuthenticatedUser(context.Background(), opt)
if err != nil {
color.Red("Error getting repos.")
return nil, err
}
allRepos = append(allRepos, repos...)
linkHeader := resp.Header.Get("link")
if linkHeader == "" || !strings.Contains(linkHeader, `rel="next"`) {
break
}
page++
}
return allRepos, nil
}
func printGitHubRepos(repos []*gh.Repository) {
t := table.NewWriter()
t.SetOutputMirror(os.Stdout)
t.AppendHeader(table.Row{"Repo Name", "Owner", "Repo Link", "Private"})
for _, repo := range repos {
if *repo.Private {
green := color.New(color.FgGreen).SprintFunc()
t.AppendRow([]interface{}{green(*repo.Name), green(*repo.Owner.Login), green(*repo.HTMLURL), green("true")})
} else {
t.AppendRow([]interface{}{*repo.Name, *repo.Owner.Login, *repo.HTMLURL, *repo.Private})
}
}
t.Render()
fmt.Print("\n\n")
}
func printGists(gists []*gh.Gist, show_all bool) {
privateCount := 0
t := table.NewWriter()
t.SetOutputMirror(os.Stdout)
t.AppendHeader(table.Row{"Gist ID", "Gist Link", "Description", "Private"})
for _, gist := range gists {
if show_all && *gist.Public {
t.AppendRow([]interface{}{*gist.ID, *gist.HTMLURL, *gist.Description, "false"})
} else if !*gist.Public {
privateCount++
green := color.New(color.FgGreen).SprintFunc()
t.AppendRow([]interface{}{green(*gist.ID), green(*gist.HTMLURL), green(*gist.Description), green("true")})
}
}
if show_all && len(gists) == 0 {
color.Red("[i] No Gist(s) Found\n")
} else if show_all {
color.Yellow("[i] Found %v Total Gist(s) (%v private)\n", len(gists), privateCount)
t.Render()
} else if privateCount == 0 {
color.Red("[i] No Private Gist(s) Found\n")
} else {
color.Green(fmt.Sprintf("[!] Found %v Private Gist(s)\n", privateCount))
t.Render()
}
fmt.Print("\n\n")
}
func getRemainingTime(t string) string {
targetTime, err := time.Parse("2006-01-02 15:04:05 MST", t)
if err != nil {
return ""
}
// Get the current time
currentTime := time.Now()
// Calculate the duration until the target time
durationUntilTarget := targetTime.Sub(currentTime)
durationUntilTarget = durationUntilTarget.Truncate(time.Minute)
// Print the duration
return fmt.Sprintf("%v", durationUntilTarget)
}
// getTokenMetadata gets the username, expiration date, and x-oauth-scopes headers for a given token
// by sending a GET request to the /user endpoint
// Returns a response object for usage in the checkFineGrained function
func getTokenMetadata(token string, client *gh.Client) (resp *gh.Response, err error) {
user, resp, err := client.Users.Get(context.Background(), "")
if err != nil {
return nil, err
}
color.Yellow("[i] Token User: %v", *user.Login)
expiry := resp.Header.Get("github-authentication-token-expiration")
timeRemaining := getRemainingTime(expiry)
if timeRemaining == "" {
color.Red("[i] Token Expiration: does not expire")
} else {
color.Yellow("[i] Token Expiration: %v (%v remaining)", expiry, timeRemaining)
}
return resp, nil
}
func checkFineGrained(resp *gh.Response, token string) (bool, error) {
// For details on token prefixes, see:
// https://github.blog/2021-04-05-behind-githubs-new-authentication-token-formats/
// Special case for ghu_ prefix tokens (ex: in a codespace) that don't have the X-OAuth-Scopes header
if strings.HasPrefix(token, "ghu_") {
color.Yellow("[i] Token Type: GitHub User-to-Server Token")
return true, nil
}
// Handle github_pat_ tokens
if strings.HasPrefix(token, "github_pat") {
color.Yellow("[i] Token Type: Fine-Grained GitHub Personal Access Token")
return true, nil
}
// Handle classic PATs
if strings.HasPrefix(token, "ghp_") {
color.Yellow("[i] Token Type: Classic GitHub Personal Access Token")
return false, nil
}
// Catch-all for any other types
// If resp.Header "X-OAuth-Scopes" doesn't exist, then we have fine-grained permissions
color.Yellow("[i] Token Type: GitHub Token")
if len(resp.Header.Values("X-Oauth-Scopes")) > 0 {
return false, nil
}
return true, nil
}
func AnalyzePermissions(cfg *config.Config, key string) {
// ToDo: Add logging for GitHub when rewrite to not use GH client.
if cfg.LoggingEnabled {
color.Red("[x] Logging not supported for GitHub Token Analysis.")
return
}
client := gh.NewClient(nil).WithAuthToken(key)
resp, err := getTokenMetadata(key, client)
if err != nil {
color.Red("[x] Invalid GitHub Token.")
return
}
// Check if the token is fine-grained or classic
if fineGrained, err := checkFineGrained(resp, key); err != nil {
color.Red("[x] Invalid GitHub Token.")
return
} else if !fineGrained {
fmt.Print("\n\n")
analyzeClassicToken(client, key, cfg.ShowAll)
} else {
fmt.Print("\n\n")
analyzeFineGrainedToken(client, key, cfg.ShowAll)
}
}

View file

@ -0,0 +1,278 @@
package gitlab
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"time"
"github.com/fatih/color"
"github.com/jedib0t/go-pretty/table"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/config"
)
// consider calling /api/v4/metadata to learn about gitlab instance version and whether neterrprises is enabled
// we'll call /api/v4/personal_access_tokens and /api/v4/user and then filter down to scopes.
type AcessTokenJSON struct {
Name string `json:"name"`
Revoked bool `json:"revoked"`
CreatedAt string `json:"created_at"`
Scopes []string `json:"scopes"`
LastUsedAt string `json:"last_used_at"`
ExpiresAt string `json:"expires_at"`
}
type ProjectsJSON struct {
NameWithNamespace string `json:"name_with_namespace"`
Permissions struct {
ProjectAccess struct {
AccessLevel int `json:"access_level"`
} `json:"project_access"`
} `json:"permissions"`
}
type ErrorJSON struct {
Error string `json:"error"`
Scope string `json:"scope"`
}
type MetadataJSON struct {
Version string `json:"version"`
Enterprise bool `json:"enterprise"`
}
func getPersonalAccessToken(cfg *config.Config, key string) (AcessTokenJSON, int, error) {
var tokens AcessTokenJSON
client := analyzers.NewAnalyzeClient(cfg)
req, err := http.NewRequest("GET", "https://gitlab.com/api/v4/personal_access_tokens/self", nil)
if err != nil {
color.Red("[x] Error: %s", err)
return tokens, -1, err
}
req.Header.Set("PRIVATE-TOKEN", key)
resp, err := client.Do(req)
if err != nil {
color.Red("[x] Error: %s", err)
return tokens, resp.StatusCode, err
}
defer resp.Body.Close()
if err := json.NewDecoder(resp.Body).Decode(&tokens); err != nil {
color.Red("[x] Error: %s", err)
return tokens, resp.StatusCode, err
}
return tokens, resp.StatusCode, nil
}
func getAccessibleProjects(cfg *config.Config, key string) ([]ProjectsJSON, error) {
var projects []ProjectsJSON
client := analyzers.NewAnalyzeClient(cfg)
req, err := http.NewRequest("GET", "https://gitlab.com/api/v4/projects", nil)
if err != nil {
color.Red("[x] Error: %s", err)
return projects, err
}
req.Header.Set("PRIVATE-TOKEN", key)
// Add query parameters
q := req.URL.Query()
q.Add("min_access_level", "10")
req.URL.RawQuery = q.Encode()
resp, err := client.Do(req)
if err != nil {
color.Red("[x] Error: %s", err)
return projects, err
}
defer resp.Body.Close()
bodyBytes, err := io.ReadAll(resp.Body)
if err != nil {
fmt.Println("Error reading the response body:", err)
return projects, err
}
newBody := func() io.ReadCloser {
return io.NopCloser(bytes.NewReader(bodyBytes))
}
if err := json.NewDecoder(newBody()).Decode(&projects); err != nil {
var e ErrorJSON
if err := json.NewDecoder(newBody()).Decode(&e); err == nil {
color.Red("[x] Insufficient Scope to query for projects. We need api or read_api permissions.\n")
return projects, nil
}
color.Red("[x] Error: %s", err)
return projects, err
}
return projects, nil
}
func getMetadata(cfg *config.Config, key string) (MetadataJSON, error) {
var metadata MetadataJSON
client := analyzers.NewAnalyzeClient(cfg)
req, err := http.NewRequest("GET", "https://gitlab.com/api/v4/metadata", nil)
if err != nil {
color.Red("[x] Error: %s", err)
return metadata, err
}
req.Header.Set("PRIVATE-TOKEN", key)
resp, err := client.Do(req)
if err != nil {
color.Red("[x] Error: %s", err)
return metadata, err
}
defer resp.Body.Close()
bodyBytes, err := io.ReadAll(resp.Body)
if err != nil {
fmt.Println("Error reading the response body:", err)
return metadata, err
}
newBody := func() io.ReadCloser {
return io.NopCloser(bytes.NewReader(bodyBytes))
}
if err := json.NewDecoder(newBody()).Decode(&metadata); err != nil {
return metadata, err
}
if metadata.Version == "" {
var e ErrorJSON
if err := json.NewDecoder(newBody()).Decode(&e); err == nil {
color.Red("[x] Insufficient Scope to query for metadata. We need read_user, ai_features, api or read_api permissions.\n")
return metadata, nil
} else {
return metadata, err
}
}
return metadata, nil
}
func AnalyzePermissions(cfg *config.Config, key string) {
// get personal_access_tokens accessible
token, statusCode, err := getPersonalAccessToken(cfg, key)
if err != nil {
color.Red("[x] Error: %s", err)
return
}
if statusCode != 200 {
color.Red("[x] Invalid GitLab Access Token")
return
}
// print token info
printTokenInfo(token)
// get metadata
metadata, err := getMetadata(cfg, key)
if err != nil {
color.Red("[x] Error: %s", err)
return
}
// print gitlab instance metadata
if metadata.Version != "" {
printMetadata(metadata)
}
// print token permissions
printTokenPermissions(token)
// get accessible projects
projects, err := getAccessibleProjects(cfg, key)
if err != nil {
color.Red("[x] Error: %s", err)
return
}
// print repos accessible
if len(projects) > 0 {
printProjects(projects)
}
}
func getRemainingTime(t string) string {
targetTime, err := time.Parse("2006-01-02", t)
if err != nil {
return ""
}
// Get the current time
currentTime := time.Now()
// Calculate the duration until the target time
durationUntilTarget := targetTime.Sub(currentTime)
durationUntilTarget = durationUntilTarget.Truncate(time.Minute)
// Print the duration
return fmt.Sprintf("%v", durationUntilTarget)
}
func printTokenInfo(token AcessTokenJSON) {
color.Green("[!] Valid GitLab Access Token\n\n")
color.Green("Token Name: %s\n", token.Name)
color.Green("Created At: %s\n", token.CreatedAt)
color.Green("Last Used At: %s\n", token.LastUsedAt)
color.Green("Expires At: %s (%v remaining)\n\n", token.ExpiresAt, getRemainingTime(token.ExpiresAt))
if token.Revoked {
color.Red("Token Revoked: %v\n", token.Revoked)
}
}
func printMetadata(metadata MetadataJSON) {
color.Green("[i] GitLab Instance Metadata\n")
color.Green("Version: %s\n", metadata.Version)
color.Green("Enterprise: %v\n\n", metadata.Enterprise)
}
func printTokenPermissions(token AcessTokenJSON) {
color.Green("[i] Token Permissions\n")
t := table.NewWriter()
t.SetOutputMirror(os.Stdout)
t.AppendHeader(table.Row{"Scope", "Access" /* Add more column headers if needed */})
for _, scope := range token.Scopes {
t.AppendRow([]interface{}{color.GreenString(scope), color.GreenString(gitlab_scopes[scope])})
}
t.SetColumnConfigs([]table.ColumnConfig{
{Number: 2, WidthMax: 100}, // Limit the width of the third column (Description) to 20 characters
})
t.Render()
}
func printProjects(projects []ProjectsJSON) {
color.Green("\n[i] Accessible Projects\n")
t := table.NewWriter()
t.SetOutputMirror(os.Stdout)
t.AppendHeader(table.Row{"Project", "Access Level" /* Add more column headers if needed */})
for _, project := range projects {
access := access_level_map[project.Permissions.ProjectAccess.AccessLevel]
if project.Permissions.ProjectAccess.AccessLevel == 50 {
access = color.GreenString(access)
} else if project.Permissions.ProjectAccess.AccessLevel >= 30 {
access = color.YellowString(access)
} else {
access = color.RedString(access)
}
t.AppendRow([]interface{}{color.GreenString(project.NameWithNamespace), access})
}
t.Render()
}

View file

@ -0,0 +1,28 @@
package gitlab
var gitlab_scopes = map[string]string{
"api": "Grants complete read/write access to the API, including all groups and projects, the container registry, the dependency proxy, and the package registry. Also grants complete read/write access to the registry and repository using Git over HTTP.",
"read_user": "Grants read-only access to the authenticated users profile through the /user API endpoint, which includes username, public email, and full name. Also grants access to read-only API endpoints under /users.",
"read_api": "Grants read access to the API, including all groups and projects, the container registry, and the package registry.",
"read_repository": "Grants read-only access to repositories on private projects using Git-over-HTTP or the Repository Files API.",
"write_repository": "Grants read-write access to repositories on private projects using Git-over-HTTP (not using the API).",
"read_registry": "Grants read-only (pull) access to container registry images if a project is private and authorization is required. Available only when the container registry is enabled.",
"write_registry": "Grants read-write (push) access to container registry images if a project is private and authorization is required. Available only when the container registry is enabled.",
"sudo": "Grants permission to perform API actions as any user in the system, when authenticated as an administrator.",
"admin_mode": "Grants permission to perform API actions as an administrator, when Admin Mode is enabled. (Introduced in GitLab 15.8.)",
"create_runner": "Grants permission to create runners.",
"manage_runner": "Grants permission to manage runners.",
"ai_features": "Grants permission to perform API actions for GitLab Duo. This scope is designed to work with the GitLab Duo Plugin for JetBrains. For all other extensions, see scope requirements.",
"k8s_proxy": "Grants permission to perform Kubernetes API calls using the agent for Kubernetes.",
"read_service_ping": "Grant access to download Service Ping payload through the API when authenticated as an admin use. (Introduced in GitLab 16.8.",
}
var access_level_map = map[int]string{
0: "No access",
5: "Minimal access",
10: "Guest",
20: "Reporter",
30: "Developer",
40: "Maintainer",
50: "Owner",
}

View file

@ -0,0 +1,409 @@
package huggingface
import (
"encoding/json"
"net/http"
"os"
"strings"
"github.com/fatih/color"
"github.com/jedib0t/go-pretty/table"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/config"
)
const (
FINEGRAINED = "fineGrained"
WRITE = "write"
READ = "read"
)
// HFTokenJSON is the struct for the HF /whoami-v2 API JSON response
type HFTokenJSON struct {
Username string `json:"name"`
Name string `json:"fullname"`
Orgs []struct {
Name string `json:"name"`
Role string `json:"roleInOrg"`
IsEnterprise bool `json:"isEnterprise"`
} `json:"orgs"`
Auth struct {
AccessToken struct {
Name string `json:"displayName"`
Type string `json:"role"`
CreatedAt string `json:"createdAt"`
FineGrained struct {
Global []string `json:"global"`
Scoped []struct {
Entity struct {
Type string `json:"type"`
Name string `json:"name"`
ID string `json:"_id"`
} `json:"entity"`
Permissions []string `json:"permissions"`
} `json:"scoped"`
} `json:"fineGrained"`
}
} `json:"auth"`
}
type Permissions struct {
Read bool
Write bool
}
type Model struct {
Name string `json:"id"`
ID string `json:"_id"`
Private bool `json:"private"`
Permissions Permissions
}
// getModelsByAuthor calls the HF API /models endpoint with the author query param
// returns a list of models and an error
func getModelsByAuthor(cfg *config.Config, key string, author string) ([]Model, error) {
var modelsJSON []Model
// create a new request
client := analyzers.NewAnalyzeClient(cfg)
req, err := http.NewRequest("GET", "https://huggingface.co/api/models", nil)
if err != nil {
return modelsJSON, err
}
// Add bearer token
req.Header.Add("Authorization", "Bearer "+key)
// Add author param
q := req.URL.Query()
q.Add("author", author)
req.URL.RawQuery = q.Encode()
// send the request
resp, err := client.Do(req)
if err != nil {
return modelsJSON, err
}
// defer the response body closing
defer resp.Body.Close()
// read response
if err := json.NewDecoder(resp.Body).Decode(&modelsJSON); err != nil {
return modelsJSON, err
}
return modelsJSON, nil
}
// getTokenInfo calls the HF API /whoami-v2 endpoint to get the token info
// returns the token info, a boolean indicating token validity, and an error
func getTokenInfo(cfg *config.Config, key string) (HFTokenJSON, bool, error) {
var tokenJSON HFTokenJSON
// create a new request
client := analyzers.NewAnalyzeClient(cfg)
req, err := http.NewRequest("GET", "https://huggingface.co/api/whoami-v2", nil)
if err != nil {
return tokenJSON, false, err
}
// Add bearer token
req.Header.Add("Authorization", "Bearer "+key)
// send the request
resp, err := client.Do(req)
if err != nil {
return tokenJSON, false, err
}
// check if the response is 200
if resp.StatusCode != 200 {
return tokenJSON, false, nil
}
// defer the response body closing
defer resp.Body.Close()
// read response
if err := json.NewDecoder(resp.Body).Decode(&tokenJSON); err != nil {
return tokenJSON, true, err
}
return tokenJSON, true, nil
}
// AnalyzePermissions prints the permissions of a HuggingFace API key
func AnalyzePermissions(cfg *config.Config, key string) {
// get token info
tokenJSON, success, err := getTokenInfo(cfg, key)
if err != nil {
color.Red("[x] Error: " + err.Error())
return
}
// check if the token is valid
if !success {
color.Red("[x] Invalid HuggingFace Access Token")
return
}
color.Green("[!] Valid HuggingFace Access Token\n\n")
// print user info
color.Yellow("[i] Username: " + tokenJSON.Username)
color.Yellow("[i] Name: " + tokenJSON.Name)
color.Yellow("[i] Token Name: " + tokenJSON.Auth.AccessToken.Name)
color.Yellow("[i] Token Type: " + tokenJSON.Auth.AccessToken.Type)
// print org info
printOrgs(tokenJSON)
// get all models by username
var allModels []Model
if userModels, err := getModelsByAuthor(cfg, key, tokenJSON.Username); err == nil {
allModels = append(allModels, userModels...)
} else {
color.Red("[x] Error: " + err.Error())
return
}
// get all models from all orgs
for _, org := range tokenJSON.Orgs {
if orgModels, err := getModelsByAuthor(cfg, key, org.Name); err == nil {
allModels = append(allModels, orgModels...)
} else {
color.Red("[x] Error: " + err.Error())
return
}
}
// print accessible models
printAccessibleModels(allModels, tokenJSON)
if tokenJSON.Auth.AccessToken.Type == FINEGRAINED {
// print org permissions
printOrgPermissions(tokenJSON)
// print user permissions
printUserPermissions(tokenJSON)
}
}
// printUserPermissions prints the user permissions
// only applies to fine-grained tokens
func printUserPermissions(tokenJSON HFTokenJSON) {
color.Green("\n[i] User Permissions:")
// build a map of all user permissions
userPermissions := map[string]struct{}{}
for _, permission := range tokenJSON.Auth.AccessToken.FineGrained.Scoped {
if permission.Entity.Type == "user" {
for _, perm := range permission.Permissions {
userPermissions[perm] = struct{}{}
}
}
}
// global permissions only apply to user tokens as of 6/6/24
// but there would be a naming collision in the scopes document
// so we prepend "global." to the key and then add to the map
for _, permission := range tokenJSON.Auth.AccessToken.FineGrained.Global {
userPermissions["global."+permission] = struct{}{}
}
// check if there are any user permissions
if len(userPermissions) == 0 {
color.Red("\tNo user permissions scoped.")
return
}
// print the user permissions
t := table.NewWriter()
t.SetOutputMirror(os.Stdout)
t.AppendHeader(table.Row{"Category", "Permission", "In-Scope"})
for _, permission := range user_scopes_order {
t.AppendRow([]interface{}{permission, "---", "---"})
for key, value := range user_scopes[permission] {
if _, ok := userPermissions[key]; ok {
t.AppendRow([]interface{}{"", color.GreenString(value), color.GreenString("True")})
} else {
t.AppendRow([]interface{}{"", value, "False"})
}
}
}
t.Render()
}
// printOrgPermissions prints the organization permissions
// only applies to fine-grained tokens
func printOrgPermissions(tokenJSON HFTokenJSON) {
color.Green("\n[i] Organization Permissions:")
// check if there are any org permissions
// if so, save them as a map. Only need to do this once
// even if multiple orgs b/c as of 6/6/24, users can only define one set of scopes
// for all orgs referenced on an access token
orgScoped := false
orgPermissions := map[string]struct{}{}
for _, permission := range tokenJSON.Auth.AccessToken.FineGrained.Scoped {
if permission.Entity.Type == "org" {
orgScoped = true
for _, perm := range permission.Permissions {
orgPermissions[perm] = struct{}{}
}
break
}
}
// check if there are any org permissions
if !orgScoped {
color.Red("\tNo organization permissions scoped.")
return
}
// print the org permissions
t := table.NewWriter()
t.SetOutputMirror(os.Stdout)
t.AppendHeader(table.Row{"Category", "Permission", "In-Scope"})
for _, permission := range org_scopes_order {
t.AppendRow([]interface{}{permission, "---", "---"})
for key, value := range org_scopes[permission] {
if _, ok := orgPermissions[key]; ok {
t.AppendRow([]interface{}{"", color.GreenString(value), color.GreenString("True")})
} else {
t.AppendRow([]interface{}{"", value, "False"})
}
}
}
t.Render()
}
// printOrgs prints the organizations the user is a member of
func printOrgs(tokenJSON HFTokenJSON) {
color.Green("\n[i] Organizations:")
if len(tokenJSON.Orgs) == 0 {
color.Yellow("\tNo organizations found.")
return
}
t := table.NewWriter()
t.SetOutputMirror(os.Stdout)
t.AppendHeader(table.Row{"Name", "Role", "Is Enterprise"})
for _, org := range tokenJSON.Orgs {
enterprise := ""
role := ""
if org.IsEnterprise {
enterprise = color.New(color.FgGreen).Sprintf("True")
} else {
enterprise = "False"
}
if org.Role == "admin" {
role = color.New(color.FgGreen).Sprintf("Admin")
} else {
role = org.Role
}
t.AppendRow([]interface{}{color.GreenString(org.Name), role, enterprise})
}
t.Render()
}
// modelNameLookup is a helper function to lookup model name by _id
func modelNameLookup(models []Model, id string) string {
for _, model := range models {
if model.ID == id {
return model.Name
}
}
return ""
}
// printAccessibleModels adds permissions as needed to each model
//
// and then calls the printModelsTable function
func printAccessibleModels(allModels []Model, tokenJSON HFTokenJSON) {
color.Green("\n[i] Accessible Models:")
if tokenJSON.Auth.AccessToken.Type != FINEGRAINED {
// Add Read Privs to All Models
for idx := range allModels {
allModels[idx].Permissions.Read = true
}
// Add Write Privs to All Models if Write Access
if tokenJSON.Auth.AccessToken.Type == WRITE {
for idx := range allModels {
allModels[idx].Permissions.Write = true
}
}
// Print Models Table
printModelsTable(allModels)
return
}
// finegrained scopes are grouped by org, user or model.
// this section will extract the relevant permissions for each entity and store them in a map
var nameToPermissions = make(map[string]Permissions)
for _, permission := range tokenJSON.Auth.AccessToken.FineGrained.Scoped {
read := false
write := false
for _, perm := range permission.Permissions {
if perm == "repo.content.read" {
read = true
} else if perm == "repo.write" {
write = true
}
}
if permission.Entity.Type == "user" || permission.Entity.Type == "org" {
nameToPermissions[permission.Entity.Name] = Permissions{Read: read, Write: write}
} else if permission.Entity.Type == "model" {
nameToPermissions[modelNameLookup(allModels, permission.Entity.ID)] = Permissions{Read: read, Write: write}
}
}
// apply permissions to all models
for idx := range allModels {
// get username/orgname for each model and apply those permissions
modelUsername := strings.Split(allModels[idx].Name, "/")[0]
if permissions, ok := nameToPermissions[modelUsername]; ok {
allModels[idx].Permissions = permissions
}
// override model permissions with repo-specific permissions
if permissions, ok := nameToPermissions[allModels[idx].Name]; ok {
allModels[idx].Permissions = permissions
}
}
// Print Models Table
printModelsTable(allModels)
}
// printModelsTable prints the models table
func printModelsTable(models []Model) {
t := table.NewWriter()
t.SetOutputMirror(os.Stdout)
t.AppendHeader(table.Row{"Model", "Private", "Read", "Write"})
for _, model := range models {
var name, read, write, private string
if model.Permissions.Read {
read = color.New(color.FgGreen).Sprintf("True")
} else {
read = "False"
}
if model.Permissions.Write {
write = color.New(color.FgGreen).Sprintf("True")
} else {
write = "False"
}
if model.Private {
private = color.New(color.FgGreen).Sprintf("True")
name = color.New(color.FgGreen).Sprintf(model.Name)
} else {
private = "False"
name = model.Name
}
t.AppendRow([]interface{}{name, private, read, write})
}
t.Render()
}

View file

@ -0,0 +1,74 @@
package huggingface
//nolint:unused
var repo_scopes = map[string]string{
"repo.content.read": "Read access to contents",
"discussion.write": "Interact with discussions / Open pull requests",
"repo.write": "Write access to contents/settings",
}
var org_scopes_order = []string{
"Repos",
"Collections",
"Inference endpoints",
"Org settings",
}
var org_scopes = map[string]map[string]string{
"Repos": {
"repo.content.read": "Read access to contents of all repos",
"discussion.write": "Interact with discussions / Open pull requests on all repos",
"repo.write": "Write access to contents/settings of all repos",
},
"Collections": {
"collection.read": "Read access to all collections",
"collection.write": "Write access to all collections",
},
"Inference endpoints": {
"inference.endpoints.infer.write": "Make calls to inference endpoints",
"inference.endpoints.write": "Manage inference endpoints",
},
"Org settings": {
"org.read": "Read access to organization's settings",
"org.write": "Write access to organization's settings / member management",
},
}
var user_scopes_order = []string{
"Billing",
"Collections",
"Discussions & Posts",
"Inference",
"Repos",
"Webhooks",
}
var user_scopes = map[string]map[string]string{
"Billing": {
"user.billing.read": "Read access to user's billing usage",
},
"Collections": {
"collection.read": "Read access to all ollections under user's namespace",
"collection.write": "Write access to all collections under user's namespace",
},
"Discussions & Posts": {
// Note: prepending global. to scopes that are nested under "global" in fine-grained permissions JSON
// otherwise they would overlap with user scopes under the "scoped" JSON
"discussion.write": "Interact with discussions / Open pull requests on repos under user's namespace",
"global.discussion.write": "Interact with discussions / Open pull requests on external repos",
"global.post.write": "Interact with posts",
},
"Inference": {
"global.inference.serverless.write": "Make calls to the serverless Inference API",
"inference.endpoints.infer.write": "Make calls to inference endpoints",
"inference.endpoints.write": "Manage inference endpoints",
},
"Repos": {
"repo.content.read": "Read access to contents of all repos under user's namespace",
"repo.write": "Write access to contents/settings of all repos under user's namespace",
},
"Webhooks": {
"user.webhooks.read": "Access webhooks data",
"user.webhooks.write": "Create and manage webhooks",
},
}

View file

@ -0,0 +1,191 @@
package mailchimp
import (
"encoding/json"
"fmt"
"net/http"
"os"
"strings"
"github.com/fatih/color"
"github.com/jedib0t/go-pretty/table"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/config"
)
var BASE_URL = "https://%s.api.mailchimp.com/3.0"
type MetadataJSON struct {
AccountID string `json:"account_id"`
AccountName string `json:"account_name"`
Email string `json:"email"`
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
Role string `json:"role"`
MemberSince string `json:"member_since"`
PricingPlan string `json:"pricing_plan_type"`
AccountTimezone string `json:"account_timezone"`
Contact struct {
Company string `json:"company"`
Address1 string `json:"addr1"`
Address2 string `json:"addr2"`
City string `json:"city"`
State string `json:"state"`
Zip string `json:"zip"`
Country string `json:"country"`
} `json:"contact"`
LastLogin string `json:"last_login"`
TotalSubscribers int `json:"total_subscribers"`
}
type DomainsJSON struct {
Domains []struct {
Domain string `json:"domain"`
Authenticated bool `json:"authenticated"`
Verified bool `json:"verified"`
} `json:"domains"`
}
func getMetadata(cfg *config.Config, key string) (MetadataJSON, error) {
var metadata MetadataJSON
// extract datacenter
keySplit := strings.Split(key, "-")
if len(keySplit) != 2 {
return metadata, nil
}
datacenter := keySplit[1]
client := analyzers.NewAnalyzeClient(cfg)
req, err := http.NewRequest("GET", fmt.Sprintf(BASE_URL, datacenter), nil)
if err != nil {
color.Red("[x] Error: %s", err)
return metadata, err
}
req.SetBasicAuth("anystring", key)
resp, err := client.Do(req)
if err != nil {
color.Red("[x] Error: %s", err)
return metadata, err
}
defer resp.Body.Close()
if err := json.NewDecoder(resp.Body).Decode(&metadata); err != nil {
color.Red("[x] Error: %s", err)
return metadata, err
}
return metadata, nil
}
func getDomains(cfg *config.Config, key string) (DomainsJSON, error) {
var domains DomainsJSON
// extract datacenter
keySplit := strings.Split(key, "-")
if len(keySplit) != 2 {
return domains, nil
}
datacenter := keySplit[1]
client := analyzers.NewAnalyzeClient(cfg)
req, err := http.NewRequest("GET", fmt.Sprintf(BASE_URL, datacenter)+"/verified-domains", nil)
if err != nil {
color.Red("[x] Error: %s", err)
return domains, err
}
req.SetBasicAuth("anystring", key)
resp, err := client.Do(req)
if err != nil {
color.Red("[x] Error: %s", err)
return domains, err
}
defer resp.Body.Close()
if err := json.NewDecoder(resp.Body).Decode(&domains); err != nil {
color.Red("[x] Error: %s", err)
return domains, err
}
return domains, nil
}
func AnalyzePermissions(cfg *config.Config, key string) {
// get metadata
metadata, err := getMetadata(cfg, key)
if err != nil {
color.Red("[x] Error: %s", err)
return
}
// print mailchimp instance metadata
if metadata.AccountID == "" {
color.Red("[x] Invalid Mailchimp API key")
return
}
printMetadata(metadata)
// print full api key permissions
color.Green("\n[i] Permissions: Full Access\n\n")
// get sending domains
domains, err := getDomains(cfg, key)
if err != nil {
color.Red("[x] Error: %s", err)
return
}
// print sending domains
if len(domains.Domains) > 0 {
printDomains(domains)
} else {
color.Yellow("[i] No sending domains found\n")
}
}
func printMetadata(metadata MetadataJSON) {
color.Green("[!] Valid Mailchimp API key\n\n")
// print table with account info
color.Yellow("[i] Mailchimp Account Info:\n")
t := table.NewWriter()
t.SetOutputMirror(os.Stdout)
t.AppendRow([]interface{}{("Account Name"), color.GreenString("%s", metadata.AccountName)})
t.AppendRow([]interface{}{("Company Name"), color.GreenString("%s", metadata.Contact.Company)})
t.AppendRow([]interface{}{("Address"), color.GreenString("%s %s\n%s, %s %s\n%s", metadata.Contact.Address1, metadata.Contact.Address2, metadata.Contact.City, metadata.Contact.State, metadata.Contact.Zip, metadata.Contact.Country)})
t.AppendRow([]interface{}{("Total Subscribers"), color.GreenString("%d", metadata.TotalSubscribers)})
t.Render()
// print user info
color.Yellow("\n[i] Mailchimp User Info:\n")
t = table.NewWriter()
t.SetOutputMirror(os.Stdout)
t.AppendRow([]interface{}{("User Name"), color.GreenString("%s %s", metadata.FirstName, metadata.LastName)})
t.AppendRow([]interface{}{("User Email"), color.GreenString("%s", metadata.Email)})
t.AppendRow([]interface{}{("User Role"), color.GreenString("%s", metadata.Role)})
t.AppendRow([]interface{}{("Last Login"), color.GreenString("%s", metadata.LastLogin)})
t.AppendRow([]interface{}{("Member Since"), color.GreenString("%s", metadata.MemberSince)})
t.Render()
}
func printDomains(domains DomainsJSON) {
color.Yellow("\n[i] Sending Domains:\n")
t := table.NewWriter()
t.SetOutputMirror(os.Stdout)
t.AppendHeader(table.Row{"Domain", "Enabled and Verified"})
for _, domain := range domains.Domains {
authenticated := ""
if domain.Authenticated && domain.Verified {
authenticated = color.GreenString("Yes")
} else {
authenticated = color.RedString("No")
}
t.AppendRow([]interface{}{color.GreenString(domain.Domain), authenticated})
}
t.Render()
}

View file

@ -0,0 +1,93 @@
package mailgun
import (
"encoding/json"
"net/http"
"os"
"strconv"
"github.com/fatih/color"
"github.com/jedib0t/go-pretty/table"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/config"
)
type Domain struct {
URL string `json:"name"`
IsDisabled bool `json:"is_disabled"`
Type string `json:"type"`
State string `json:"state"`
CreatedAt string `json:"created_at"`
}
type DomainsJSON struct {
Items []Domain `json:"items"`
TotalCount int `json:"total_count"`
}
func getDomains(cfg *config.Config, apiKey string) (DomainsJSON, int, error) {
var domainsJSON DomainsJSON
client := analyzers.NewAnalyzeClient(cfg)
req, err := http.NewRequest("GET", "https://api.mailgun.net/v4/domains", nil)
if err != nil {
return domainsJSON, -1, err
}
req.SetBasicAuth("api", apiKey)
resp, err := client.Do(req)
if err != nil {
return domainsJSON, -1, err
}
if resp.StatusCode != 200 {
return domainsJSON, resp.StatusCode, nil
}
defer resp.Body.Close()
err = json.NewDecoder(resp.Body).Decode(&domainsJSON)
if err != nil {
return domainsJSON, resp.StatusCode, err
}
return domainsJSON, resp.StatusCode, nil
}
func AnalyzePermissions(cfg *config.Config, apiKey string) {
// Get the domains associated with the API key
domains, statusCode, err := getDomains(cfg, apiKey)
if err != nil {
color.Red("[x] Error getting domains: %s", err)
return
}
if statusCode != 200 {
color.Red("[x] Invalid Mailgun API key.")
return
}
color.Green("[i] Valid Mailgun API key\n\n")
color.Green("[i] Permissions: Full Access\n\n")
// Print the domains
printDomains(domains)
}
func printDomains(domains DomainsJSON) {
if domains.TotalCount == 0 {
color.Red("[i] No domains found")
return
}
color.Yellow("[i] Found %d domain(s)", domains.TotalCount)
t := table.NewWriter()
t.SetOutputMirror(os.Stdout)
t.AppendHeader(table.Row{"Domain", "Type", "State", "Created At", "Disabled"})
for _, domain := range domains.Items {
if domain.IsDisabled {
t.AppendRow([]interface{}{color.RedString(domain.URL), color.RedString(domain.Type), color.RedString(domain.State), color.RedString(domain.CreatedAt), color.RedString(strconv.FormatBool(domain.IsDisabled))})
} else if domain.Type == "sandbox" || domain.State == "unverified" {
t.AppendRow([]interface{}{color.YellowString(domain.URL), color.YellowString(domain.Type), color.YellowString(domain.State), color.YellowString(domain.CreatedAt), color.YellowString(strconv.FormatBool(domain.IsDisabled))})
} else {
t.AppendRow([]interface{}{color.GreenString(domain.URL), color.GreenString(domain.Type), color.GreenString(domain.State), color.GreenString(domain.CreatedAt), color.GreenString(strconv.FormatBool(domain.IsDisabled))})
}
}
t.Render()
}

View file

@ -0,0 +1,775 @@
package mysql
import (
"database/sql"
"fmt"
"os"
"strings"
"time"
"github.com/dustin/go-humanize"
_ "github.com/go-sql-driver/mysql"
"github.com/jedib0t/go-pretty/text"
"github.com/jedib0t/go-pretty/v6/table"
"github.com/xo/dburl"
"github.com/fatih/color"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/config"
)
const (
// MySQL SSL Modes
mysql_sslmode = "ssl-mode"
mysql_sslmode_disabled = "DISABLED"
mysql_sslmode_preferred = "PREFERRED"
mysql_sslmode_required = "REQUIRED"
mysql_sslmode_verify_ca = "VERIFY_CA"
mysql_sslmode_verify_identity = "VERIFY_IDENTITY"
//https://github.com/go-sql-driver/mysql/issues/899#issuecomment-443493840
// MySQL Built-in Databases
mysql_db_sys = "sys"
mysql_db_perf_sch = "performance_schema"
mysql_db_info_sch = "information_schema"
mysql_db_mysql = "mysql"
mysql_all = "*"
)
type GlobalPrivs struct {
Privs []string
}
type Database struct {
Name string
Default bool
Tables *[]Table
Privs []string
Routines *[]Routine
Nonexistent bool
}
type Table struct {
Name string
Columns []Column
Privs []string
Nonexistent bool
Bytes uint64
}
type Column struct {
Name string
Privs []string
}
type Routine struct {
Name string
Privs []string
Nonexistent bool
}
// so CURRENT_USER returns `doadmin@%` and not `doadmin@localhost
// USER() returns `doadmin@localhost`
func AnalyzePermissions(cfg *config.Config, connectionStr string) {
// ToDo: Add in logging
if cfg.LoggingEnabled {
color.Red("[x] Logging is not supported for this analyzer.")
return
}
db, err := createConnection(connectionStr)
if err != nil {
color.Red("[!] Error connecting to the MySQL database: %s", err)
return
}
defer db.Close()
// Get the current user
user, err := getUser(db)
if err != nil {
color.Red("[!] Error getting the current user: %s", err)
return
}
color.Green("[+] Successfully connected as user: %s", user)
// Get all accessible databases
var databases = make(map[string]*Database, 0)
err = getDatabases(db, databases)
if err != nil {
color.Red("[!] Error getting databases: %s", err)
return
}
//Get all accessible tables
err = getTables(db, databases)
if err != nil {
color.Red("[!] Error getting tables: %s", err)
return
}
// Get all accessible routines
err = getRoutines(db, databases)
if err != nil {
color.Red("[!] Error getting routines: %s", err)
return
}
// Get user grants
grants, err := getGrants(db)
if err != nil {
color.Red("[!] Error getting user grants: %s", err)
return
}
var globalPrivs GlobalPrivs
// Process user grants
processGrants(grants, databases, &globalPrivs)
// Print the results
printResults(databases, globalPrivs, cfg.ShowAll)
// Build print function, check data, and then review all of the logic.
// Then make sure we have an instance of lal of that logic to actually test.
}
func createConnection(connection string) (*sql.DB, error) {
// Check if the connection string starts with 'mysql://'
if !strings.HasPrefix(connection, "mysql://") {
color.Yellow("[i] The connection string should start with 'mysql://'. Adding it for you.")
connection = "mysql://" + connection
}
// Adapt ssl-mode params to Go MySQL driver
connection, err := fixTLSQueryParam(connection)
if err != nil {
return nil, err
}
// Parse the connection string
u, err := dburl.Parse(connection)
if err != nil {
return nil, err
}
// Connect to the MySQL database
db, err := sql.Open("mysql", u.DSN)
if err != nil {
return nil, err
}
db.SetConnMaxLifetime(time.Minute * 5)
db.SetMaxOpenConns(10)
db.SetMaxIdleConns(10)
// Check the connection
err = db.Ping()
if err != nil {
if strings.Contains(err.Error(), "certificate signed by unknown authority") {
return nil, fmt.Errorf("%s. try adding 'ssl-mode=PREFERRED' to your connection string", err.Error())
}
return nil, err
}
return db, nil
}
func fixTLSQueryParam(connection string) (string, error) {
// Parse connection string on "?"
parsed := strings.Split(connection, "?")
// Check if has query parms
if len(parsed) < 2 {
// Add 10s timeout
connection += "?timeout=10s"
return connection, nil
}
var error error
// Split parms
querySlice := strings.Split(parsed[1], "&")
// Check if ssl-mode is present
for i, part := range querySlice {
if strings.HasPrefix(part, "ssl-mode") {
mode := strings.Split(part, "=")[1]
switch mode {
case mysql_sslmode_disabled:
querySlice[i] = "tls=false"
case mysql_sslmode_preferred:
querySlice[i] = "tls=preferred"
case mysql_sslmode_required:
querySlice[i] = "tls=true"
case mysql_sslmode_verify_ca:
error = fmt.Errorf("this implementation does not support VERIFY_CA. try removing it or using ssl-mode=REQUIRED")
// Need to implement --ssl-ca or --ssl-capath
case mysql_sslmode_verify_identity:
error = fmt.Errorf("this implementation does not support VERIFY_IDENTITY. try removing it or using ssl-mode=REQUIRED")
// Need to implement --ssl-ca or --ssl-capath
}
}
}
// Join the parts back together
newQuerySlice := strings.Join(querySlice, "&")
return (parsed[0] + "?" + newQuerySlice + "&timeout=10s"), error
}
func getUser(db *sql.DB) (string, error) {
var user string
err := db.QueryRow("SELECT CURRENT_USER()").Scan(&user)
if err != nil {
return "", err
}
return user, nil
}
func getDatabases(db *sql.DB, databases map[string]*Database) error {
rows, err := db.Query("SHOW DATABASES")
if err != nil {
return err
}
defer rows.Close()
for rows.Next() {
var dbName string
err = rows.Scan(&dbName)
if err != nil {
return err
}
// check if the database is a built-in database
built_in_db := false
switch dbName {
case mysql_db_sys, mysql_db_perf_sch, mysql_db_info_sch, mysql_db_mysql:
built_in_db = true
}
// add the database to the databases map
newTables := make([]Table, 0)
newRoutines := make([]Routine, 0)
databases[dbName] = &Database{Name: dbName, Default: built_in_db, Tables: &newTables, Routines: &newRoutines}
}
return nil
}
func getTables(db *sql.DB, databases map[string]*Database) error {
rows, err := db.Query("SELECT table_schema, table_name, IFNULL(DATA_LENGTH,0) FROM information_schema.tables")
if err != nil {
return err
}
defer rows.Close()
for rows.Next() {
var dbName string
var tableName string
var tableSize uint64
err = rows.Scan(&dbName, &tableName, &tableSize)
if err != nil {
return err
}
// find the database in the databases slice
d := databases[dbName]
*d.Tables = append(*d.Tables, Table{Name: tableName, Bytes: tableSize})
}
return nil
}
func getRoutines(db *sql.DB, databases map[string]*Database) error {
rows, err := db.Query("SELECT routine_schema, routine_name FROM information_schema.routines")
if err != nil {
return err
}
defer rows.Close()
for rows.Next() {
var dbName string
var routineName string
err = rows.Scan(&dbName, &routineName)
if err != nil {
return err
}
// find the database in the databases slice
d, ok := databases[dbName]
if !ok {
databases[dbName] = &Database{Name: dbName, Default: false, Tables: &[]Table{}, Routines: &[]Routine{}, Nonexistent: true}
d = databases[dbName]
}
*d.Routines = append(*d.Routines, Routine{Name: routineName})
}
return nil
}
func getGrants(db *sql.DB) ([]string, error) {
rows, err := db.Query("SHOW GRANTS")
if err != nil {
return nil, err
}
defer rows.Close()
var grants []string
for rows.Next() {
var grant string
err = rows.Scan(&grant)
if err != nil {
return nil, err
}
grants = append(grants, grant)
}
return grants, nil
}
// ToDo: Deal with these GRANT/REVOKE statements
// GRANT SELECT (col1), INSERT (col1, col2) ON mydb.mytbl TO 'someuser'@'somehost';
// GRANT PROXY ON 'localuser'@'localhost' TO 'externaluser'@'somehost';
// GRANT 'role1', 'role2' TO 'user1'@'localhost', 'user2'@'localhost';
// What are the default privs on information_schema and performance_Schema?
// Seems table by table...maybe just put "Not Implemented" and leave this to be a show_all option.
// Note: Can't GRANT on a table that doesn't exist, but DB is fine.
// processGrants processes the grants and adds them to the databases structs and globalPrivs
func processGrants(grants []string, databases map[string]*Database, globalPrivs *GlobalPrivs) {
for _, grant := range grants {
// GRANTs on non-existent databases are valid, but we need that object to exist in "databases" for processGrant().
db := parseDBFromGrant(grant)
if db == mysql_all {
continue
}
_, ok := databases[db]
if !ok {
databases[db] = &Database{Name: db, Default: false, Tables: &[]Table{}, Routines: &[]Routine{}, Nonexistent: true}
}
}
for _, grant := range grants {
processGrant(grant, databases, globalPrivs)
}
}
func processGrant(grant string, databases map[string]*Database, globalPrivs *GlobalPrivs) {
isGrant := strings.HasPrefix(grant, "GRANT")
//hasGrantOption := strings.HasSuffix(grant, "WITH GRANT OPTION")
// remove GRANT or REVOKE
grant = strings.TrimPrefix(grant, "GRANT")
grant = strings.TrimPrefix(grant, "REVOKE")
// Split on " ON "
parts := strings.Split(grant, " ON ")
if len(parts) < 2 {
color.Red("[!] Error processing grant: %s", grant)
return
}
// Put privs in a slice
privs := strings.Split(parts[0], ",")
for i, priv := range privs {
privs[i] = strings.Trim(priv, " ")
}
// Get DB and Table
dbName := strings.Trim(strings.Split(parts[1], " TO ")[0], " ")
if dbName == parts[1] {
dbName = strings.Trim(strings.Split(parts[1], " FROM ")[0], " ")
}
// Find the database in the databases slice
// Note: table may not exist yet OR may be a routine
dbTableParts := strings.Split(dbName, ".")
db := strings.Trim(dbTableParts[0], "\"`")
table := strings.Trim(dbTableParts[1], "\"`")
// dont' forget to deal with revoking db-level privs
if db == mysql_all {
// Deal with "ALL" and "ALL PRIVILEGES"
switch privs[0] {
case "ALL", "ALL PRIVILEGES":
addRemoveAllPrivs(databases, globalPrivs, isGrant)
default:
for _, priv := range privs {
addRemoveOnePrivOnAll(databases, globalPrivs, priv, isGrant)
}
}
} else {
// Check if the privs are for a routine
isRoutine := checkIsRoutine(privs)
if isRoutine {
db = strings.TrimPrefix(db, "PROCEDURE `")
db = strings.TrimSuffix(db, "`")
}
d := databases[db]
switch {
case table == mysql_all:
filteredDBPrivs := filterDBPrivs(privs)
filteredTablePrivs := filterTablePrivs(privs)
d.Privs = addRemovePrivs(d.Privs, filteredDBPrivs, isGrant)
for i, t := range *d.Tables {
(*d.Tables)[i].Privs = addRemovePrivs(t.Privs, filteredTablePrivs, isGrant)
}
case isRoutine:
var idx = getRoutineIndex(d, table)
if idx == -1 {
*d.Routines = append(*d.Routines, Routine{Name: table, Nonexistent: true})
idx = len(*d.Routines) - 1
}
(*d.Routines)[idx].Privs = addRemovePrivs((*d.Routines)[idx].Privs, privs, isGrant)
default:
var idx = getTableIndex(d, table)
if idx == -1 {
*d.Tables = append(*d.Tables, Table{Name: table, Nonexistent: true, Bytes: 0})
idx = len(*d.Tables) - 1
}
(*d.Tables)[idx].Privs = addRemovePrivs((*d.Tables)[idx].Privs, privs, isGrant)
}
}
}
func parseDBFromGrant(grant string) string {
// Split on " ON "
parts := strings.Split(grant, " ON ")
if len(parts) < 2 {
color.Red("[!] Error processing grant: %s", grant)
return ""
}
// Get DB and Table
dbName := strings.Trim(strings.Split(parts[1], " TO ")[0], " ")
if dbName == parts[1] {
dbName = strings.Trim(strings.Split(parts[1], " FROM ")[0], " ")
}
dbTableParts := strings.Split(dbName, ".")
db := strings.Trim(dbTableParts[0], "\"`")
db = strings.TrimPrefix(db, "PROCEDURE `")
db = strings.TrimSuffix(db, "`")
return db
}
func filterDBPrivs(privs []string) []string {
filtered := make([]string, 0)
for _, priv := range privs {
if SCOPES[priv].Database {
filtered = append(filtered, priv)
}
}
return filtered
}
func filterTablePrivs(privs []string) []string {
filtered := make([]string, 0)
for _, priv := range privs {
if SCOPES[priv].Table {
filtered = append(filtered, priv)
}
}
return filtered
}
func addRemoveOnePrivOnAll(databases map[string]*Database, globalPrivs *GlobalPrivs, priv string, isGrant bool) {
scope, ok := SCOPES[priv]
if !ok {
color.Red("[!] Error processing grant: privilege doesn't exist in our MySQL (%s)", priv)
return
}
slicedPriv := []string{priv}
// Add priv to globalPrivs
if scope.Global {
globalPrivs.Privs = addRemovePrivs(globalPrivs.Privs, slicedPriv, isGrant)
}
// Add/Remove priv to all databases
if scope.Database {
for _, d := range databases {
if d.Name == "information_schema" || d.Name == "performance_schema" {
continue
}
d.Privs = addRemovePrivs(d.Privs, slicedPriv, isGrant)
}
}
// Add/Remove priv to all tables
if scope.Table {
for _, d := range databases {
for i, t := range *d.Tables {
(*d.Tables)[i].Privs = addRemovePrivs(t.Privs, slicedPriv, isGrant)
}
}
}
// Add/Remove priv to all routines
if scope.Routine {
for _, d := range databases {
for i, r := range *d.Routines {
(*d.Routines)[i].Privs = addRemovePrivs(r.Privs, slicedPriv, isGrant)
}
}
}
}
func addRemoveAllPrivs(databases map[string]*Database, globalPrivs *GlobalPrivs, isGrant bool) {
// Add all privs to globalPrivs
globalAllPrivs := getGlobalAllPrivileges()
globalPrivs.Privs = addRemovePrivs(globalPrivs.Privs, globalAllPrivs, isGrant)
// Get DB, Table and Routine Privs
dbAllPrivs := getDBAllPrivs()
tableAllPrivs := getTableAllPrivs()
routineAllPrivs := getRoutineAllPrivs()
// Add all privs to all databases and tables and routines
for _, d := range databases {
if d.Name == "information_schema" || d.Name == "performance_schema" {
continue
}
// Add DB-level privs
d.Privs = addRemovePrivs(d.Privs, dbAllPrivs, isGrant)
// Add Table-level privs
for i, t := range *d.Tables {
(*d.Tables)[i].Privs = addRemovePrivs(t.Privs, tableAllPrivs, isGrant)
}
// Add Routine-level privs
for i, r := range *d.Routines {
(*d.Routines)[i].Privs = addRemovePrivs(r.Privs, routineAllPrivs, isGrant)
}
}
}
func getGlobalAllPrivileges() []string {
privs := make([]string, 0)
for priv, scope := range SCOPES {
if scope.Global && !scope.Dynamic && priv != "USAGE" && priv != "GRANT OPTION" {
privs = append(privs, priv)
}
}
return privs
}
func getDBAllPrivs() []string {
privs := make([]string, 0)
for priv, scope := range SCOPES {
if scope.Database && !scope.Dynamic && priv != "USAGE" && priv != "GRANT OPTION" {
privs = append(privs, priv)
}
}
return privs
}
func getTableAllPrivs() []string {
privs := make([]string, 0)
for priv, scope := range SCOPES {
if scope.Table && !scope.Dynamic && priv != "USAGE" && priv != "GRANT OPTION" {
privs = append(privs, priv)
}
}
return privs
}
func getRoutineAllPrivs() []string {
privs := make([]string, 0)
for priv, scope := range SCOPES {
if scope.Routine && !scope.Dynamic && priv != "USAGE" && priv != "GRANT OPTION" {
privs = append(privs, priv)
}
}
return privs
}
func checkIsRoutine(privs []string) bool {
if len(privs) > 0 {
return SCOPES[privs[0]].Routine
}
return false
}
func getTableIndex(d *Database, tableName string) int {
for i, t := range *d.Tables {
if t.Name == tableName {
return i
}
}
return -1
}
func getRoutineIndex(d *Database, routineName string) int {
for i, r := range *d.Routines {
if r.Name == routineName {
return i
}
}
return -1
}
func addRemovePrivs(currentPrivs []string, privsToAddRemove []string, add bool) []string {
newPrivs := make([]string, 0)
if add {
newPrivs = append(currentPrivs, privsToAddRemove...)
return newPrivs
}
for _, p := range currentPrivs {
found := false
for _, p2 := range privsToAddRemove {
if p == p2 {
found = true
break
}
}
if !found {
newPrivs = append(newPrivs, p)
}
}
return newPrivs
}
func printResults(databases map[string]*Database, globalPrivs GlobalPrivs, showAll bool) {
// Print Global Privileges
printGlobalPrivs(globalPrivs)
// Print Database and Table Privileges
printDBTablePrivs(databases, showAll)
// Print Routine Privileges
printRoutinePrivs(databases, showAll)
}
func printGlobalPrivs(globalPrivs GlobalPrivs) {
// Prep table writer
t := table.NewWriter()
t.SetOutputMirror(os.Stdout)
t.AppendHeader(table.Row{"Global Privileges"})
// Print global privs
globalPrivsStr := ""
for _, priv := range globalPrivs.Privs {
globalPrivsStr += priv + ", "
}
// Clean up privs string
globalPrivsStr = cleanPrivStr(globalPrivsStr)
// Add rows of priv string data
t.AppendRow([]interface{}{analyzers.GreenWriter(text.WrapSoft(globalPrivsStr, 100))})
t.Render()
}
func printDBTablePrivs(databases map[string]*Database, showAll bool) {
// Prep table writer
t := table.NewWriter()
t.SetOutputMirror(os.Stdout)
t.AppendHeader(table.Row{"Database", "Table", "Privileges", "Est. Size"})
// Print database privs
for _, d := range databases {
if isBuiltIn(d.Name) && !showAll {
continue
}
// Add privileges to db or table privs strings
dbPrivsStr := ""
dbTablesStr := ""
for _, priv := range d.Privs {
scope := SCOPES[priv]
if scope.Database && scope.Table {
dbTablesStr += priv + ", "
} else {
dbPrivsStr += priv + ", "
}
}
// Clean up privs strings
dbPrivsStr = cleanPrivStr(dbPrivsStr)
dbTablesStr = cleanPrivStr(dbTablesStr)
// Prep String colors
var dbName string
var writer func(a ...interface{}) string
if d.Default {
dbName = d.Name + " (built-in)"
writer = analyzers.YellowWriter
} else if d.Nonexistent {
dbName = d.Name + " (nonexistent)"
writer = analyzers.RedWriter
} else {
dbName = d.Name
writer = analyzers.GreenWriter
}
// Prep Priv Strings
// Add rows of priv string data
t.AppendRow([]interface{}{writer(dbName), writer("<DB-Level Privs>"), writer(text.WrapSoft(dbPrivsStr, 80)), writer("-")})
t.AppendRow([]interface{}{"", writer("<All tables>"), writer(text.WrapSoft(dbTablesStr, 80)), writer("-")})
// Print table privs
for _, t2 := range *d.Tables {
tablePrivsStr := ""
for _, priv := range t2.Privs {
tablePrivsStr += priv + ", "
}
tablePrivsStr = cleanPrivStr(tablePrivsStr)
t.AppendRow([]interface{}{"", writer(t2.Name), writer(text.WrapSoft(tablePrivsStr, 80)), writer(humanize.Bytes(t2.Bytes))})
}
// Add a separator between databases
t.AppendSeparator()
}
t.Render()
}
func printRoutinePrivs(databases map[string]*Database, showAll bool) {
// Print routine privs
t := table.NewWriter()
t.SetOutputMirror(os.Stdout)
t.AppendHeader(table.Row{"Database", "Routine", "Privileges"})
// Add rows of priv string data
for _, d := range databases {
if isBuiltIn(d.Name) && !showAll {
continue
}
for _, r := range *d.Routines {
routinePrivsStr := ""
for _, priv := range r.Privs {
routinePrivsStr += priv + ", "
}
routinePrivsStr = cleanPrivStr(routinePrivsStr)
var writer func(a ...interface{}) string
switch d.Name {
case mysql_db_info_sch, mysql_db_perf_sch, mysql_db_sys, mysql_db_mysql:
writer = analyzers.YellowWriter
default:
writer = analyzers.GreenWriter
}
t.AppendRow([]interface{}{writer(d.Name), writer(r.Name), writer(text.WrapSoft(routinePrivsStr, 80))})
}
}
t.Render()
}
func cleanPrivStr(priv string) string {
priv = strings.TrimSuffix(priv, ", ")
if priv == "" {
priv = "-"
}
return priv
}
func isBuiltIn(dbName string) bool {
switch dbName {
case mysql_db_sys, mysql_db_perf_sch, mysql_db_info_sch, mysql_db_mysql:
return true
}
return false
}

View file

@ -0,0 +1,99 @@
package mysql
type PrivTypes struct {
Global bool
Database bool
Table bool
Column bool
Routine bool
Proxy bool
Dynamic bool
}
// https://dev.mysql.com/doc/refman/8.0/en/grant.html#grant-global-privileges:~:text=%27localhost%27%3B-,Privileges%20Supported%20by%20MySQL,-The%20following%20tables
var SCOPES = map[string]PrivTypes{
// Static privs
"ALTER": {Global: true, Database: true, Table: true},
"ALTER ROUTINE": {Global: true, Database: true, Routine: true},
"CREATE": {Global: true, Database: true, Table: true},
"CREATE ROLE": {Global: true},
"CREATE ROUTINE": {Global: true, Database: true},
"CREATE TABLESPACE": {Global: true},
"CREATE TEMPORARY TABLES": {Global: true, Database: true},
"CREATE USER": {Global: true},
"CREATE VIEW": {Global: true, Database: true, Table: true},
"DELETE": {Global: true, Database: true, Table: true},
"DROP": {Global: true, Database: true, Table: true},
"DROP ROLE": {Global: true},
"EVENT": {Global: true, Database: true},
"EXECUTE": {Global: true, Database: true, Routine: true},
"FILE": {Global: true},
"GRANT OPTION": {Global: true, Database: true, Table: true, Routine: true, Proxy: true}, // Not granted on ALL PRIVILEGES
"INDEX": {Global: true, Database: true, Table: true},
"INSERT": {Global: true, Database: true, Table: true, Column: true},
"LOCK TABLES": {Global: true, Database: true},
"PROCESS": {Global: true},
"PROXY": {Proxy: true}, // Not granted on ALL PRIVILEGES
"REFERENCES": {Global: true, Database: true, Table: true, Column: true},
"RELOAD": {Global: true},
"REPLICATION CLIENT": {Global: true},
"REPLICATION SLAVE": {Global: true},
"SELECT": {Global: true, Database: true, Table: true, Column: true},
"SHOW DATABASES": {Global: true},
"SHOW VIEW": {Global: true, Database: true, Table: true},
"SHUTDOWN": {Global: true},
"SUPER": {Global: true},
"TRIGGER": {Global: true, Database: true, Table: true},
"UPDATE": {Global: true, Database: true, Table: true, Column: true},
// This is a special case, it's not a real privilege
"USAGE": {Global: true, Database: true, Table: true, Column: true, Routine: true},
// Dynamic privs
"ALLOW_NONEXISTENT_DEFINER": {Global: true, Dynamic: true},
"APPLICATION_PASSWORD_ADMIN": {Global: true, Dynamic: true},
"AUDIT_ABORT_EXEMPT": {Global: true, Dynamic: true},
"AUDIT_ADMIN": {Global: true, Dynamic: true},
"AUTHENTICATION_POLICY_ADMIN": {Global: true, Dynamic: true},
"BACKUP_ADMIN": {Global: true, Dynamic: true},
"BINLOG_ADMIN": {Global: true, Dynamic: true},
"BINLOG_ENCRYPTION_ADMIN": {Global: true, Dynamic: true},
"CLONE_ADMIN": {Global: true, Dynamic: true},
"CONNECTION_ADMIN": {Global: true, Dynamic: true},
"ENCRYPTION_KEY_ADMIN": {Global: true, Dynamic: true},
"FIREWALL_ADMIN": {Global: true, Dynamic: true},
"FIREWALL_EXEMPT": {Global: true, Dynamic: true},
"FIREWALL_USER": {Global: true, Dynamic: true},
"FLUSH_OPTIMIZER_COSTS": {Global: true, Dynamic: true},
"FLUSH_STATUS": {Global: true, Dynamic: true},
"FLUSH_TABLES": {Global: true, Dynamic: true},
"FLUSH_USER_RESOURCES": {Global: true, Dynamic: true},
"GROUP_REPLICATION_ADMIN": {Global: true, Dynamic: true},
"GROUP_REPLICATION_STREAM": {Global: true, Dynamic: true},
"INNODB_REDO_LOG_ARCHIVE": {Global: true, Dynamic: true},
"INNODB_REDO_LOG_ENABLE": {Global: true, Dynamic: true},
"MASKING_DICTIONARIES_ADMIN": {Global: true, Dynamic: true},
"NDB_STORED_USER": {Global: true, Dynamic: true},
"PASSWORDLESS_USER_ADMIN": {Global: true, Dynamic: true},
"PERSIST_RO_VARIABLES_ADMIN": {Global: true, Dynamic: true},
"REPLICATION_APPLIER": {Global: true, Dynamic: true},
"REPLICATION_SLAVE_ADMIN": {Global: true, Dynamic: true},
"RESOURCE_GROUP_ADMIN": {Global: true, Dynamic: true},
"RESOURCE_GROUP_USER": {Global: true, Dynamic: true},
"ROLE_ADMIN": {Global: true, Dynamic: true},
"SENSITIVE_VARIABLES_OBSERVER": {Global: true, Dynamic: true},
"SERVICE_CONNECTION_ADMIN": {Global: true, Dynamic: true},
"SESSION_VARIABLES_ADMIN": {Global: true, Dynamic: true},
"SET_ANY_DEFINER": {Global: true, Dynamic: true},
"SET_USER_ID": {Global: true, Dynamic: true},
"SHOW_ROUTINE": {Global: true, Dynamic: true},
"SKIP_QUERY_REWRITE": {Global: true, Dynamic: true},
"SYSTEM_USER": {Global: true, Dynamic: true},
"SYSTEM_VARIABLES_ADMIN": {Global: true, Dynamic: true},
"TABLE_ENCRYPTION_ADMIN": {Global: true, Dynamic: true},
"TELEMETRY_LOG_ADMIN": {Global: true, Dynamic: true},
"TP_CONNECTION_ADMIN": {Global: true, Dynamic: true},
"TRANSACTION_GTID_TAG": {Global: true, Dynamic: true},
"VERSION_TOKEN_ADMIN": {Global: true, Dynamic: true},
"XA_RECOVER_ADMIN": {Global: true, Dynamic: true},
}

View file

@ -0,0 +1,204 @@
package openai
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"github.com/fatih/color"
"github.com/jedib0t/go-pretty/table"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/config"
)
const (
BASE_URL = "https://api.openai.com"
ORGS_ENDPOINT = "/v1/organizations"
ME_ENDPOINT = "/v1/me"
)
type MeJSON struct {
ID string `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
Phone string `json:"phone_number"`
MfaEnabled bool `json:"mfa_flag_enabled"`
Orgs struct {
Data []struct {
Title string `json:"title"`
} `json:"data"`
} `json:"orgs"`
}
var POST_PAYLOAD = map[string]interface{}{"speed": 1}
// AnalyzePermissions will analyze the permissions of an OpenAI API key
func AnalyzePermissions(cfg *config.Config, key string) {
if meJSON, err := getUserData(cfg, key); err != nil {
color.Red("[x]" + err.Error())
return
} else {
printUserData(meJSON)
}
if isAdmin, err := checkAdminKey(cfg, key); isAdmin {
color.Green("[!] Admin API Key. All permissions available.")
return
} else if err != nil {
color.Red("[x]" + err.Error())
return
} else {
color.Yellow("[!] Restricted API Key. Limited permissions available.")
if err := analyzeScopes(key); err != nil {
color.Red("[x]" + err.Error())
return
}
printPermissions(cfg.ShowAll)
}
}
func analyzeScopes(key string) error {
for _, scope := range SCOPES {
if err := scope.RunTests(key); err != nil {
return err
}
}
return nil
}
func openAIRequest(cfg *config.Config, method string, url string, key string, data map[string]interface{}) ([]byte, *http.Response, error) {
var inBody io.Reader
if data != nil {
jsonData, err := json.Marshal(data)
if err != nil {
return nil, nil, err
}
inBody = bytes.NewBuffer(jsonData)
}
client := analyzers.NewAnalyzeClient(cfg)
req, err := http.NewRequest(method, url, inBody)
if err != nil {
return nil, nil, err
}
req.Header.Add("Authorization", "Bearer "+key)
req.Header.Add("Content-Type", "application/json")
resp, err := client.Do(req)
if err != nil {
return nil, nil, err
}
defer resp.Body.Close()
outBody, err := io.ReadAll(resp.Body)
if err != nil {
return nil, nil, err
}
return outBody, resp, nil
}
func checkAdminKey(cfg *config.Config, key string) (bool, error) {
// Check for all permissions
//nolint:bodyclose
_, resp, err := openAIRequest(cfg, "GET", BASE_URL+ORGS_ENDPOINT, key, nil)
if err != nil {
return false, err
}
switch resp.StatusCode {
case 200:
return true, nil
case 403:
return false, nil
default:
return false, err
}
}
func getUserData(cfg *config.Config, key string) (MeJSON, error) {
var meJSON MeJSON
//nolint:bodyclose
me, resp, err := openAIRequest(cfg, "GET", BASE_URL+ME_ENDPOINT, key, nil)
if err != nil {
return meJSON, err
}
if resp.StatusCode != 200 {
return meJSON, fmt.Errorf("Invalid OpenAI Token")
}
color.Green("[!] Valid OpenAI Token\n\n")
// Marshall me into meJSON struct
if err := json.Unmarshal(me, &meJSON); err != nil {
return meJSON, err
}
return meJSON, nil
}
func printUserData(meJSON MeJSON) {
color.Green("[i] User: %v", meJSON.Name)
color.Green("[i] Email: %v", meJSON.Email)
color.Green("[i] Phone: %v", meJSON.Phone)
color.Green("[i] MFA Enabled: %v", meJSON.MfaEnabled)
if len(meJSON.Orgs.Data) > 0 {
color.Green("[i] Organizations:")
for _, org := range meJSON.Orgs.Data {
color.Green(" - %v", org.Title)
}
}
fmt.Print("\n\n")
}
func stringifyPermissionStatus(tests []analyzers.HttpStatusTest) analyzers.PermissionType {
readStatus := false
writeStatus := false
errors := false
for _, test := range tests {
if test.Type == analyzers.READ {
readStatus = test.Status.Value
} else if test.Type == analyzers.WRITE {
writeStatus = test.Status.Value
}
if test.Status.IsError {
errors = true
}
}
if errors {
return analyzers.ERROR
}
if readStatus && writeStatus {
return analyzers.READ_WRITE
} else if readStatus {
return analyzers.READ
} else if writeStatus {
return analyzers.WRITE
} else {
return analyzers.NONE
}
}
func printPermissions(show_all bool) {
fmt.Print("\n\n")
t := table.NewWriter()
t.SetOutputMirror(os.Stdout)
t.AppendHeader(table.Row{"Scope", "Endpoints", "Permission"})
for _, scope := range SCOPES {
status := stringifyPermissionStatus(scope.Tests)
writer := analyzers.GetWriterFromStatus(status)
if show_all || status != analyzers.NONE {
t.AppendRow([]interface{}{writer(scope.Name), writer(scope.Endpoints[0]), writer(status)})
for i := 1; i < len(scope.Endpoints); i++ {
t.AppendRow([]interface{}{"", writer(scope.Endpoints[i]), writer(status)})
}
}
}
t.Render()
fmt.Print("\n\n")
}

View file

@ -0,0 +1,72 @@
package openai
import "github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers"
type OpenAIScope struct {
Name string
Tests []analyzers.HttpStatusTest
Endpoints []string
}
func (s *OpenAIScope) RunTests(key string) error {
headers := map[string]string{
"Authorization": "Bearer " + key,
"Content-Type": "application/json",
}
for i := range s.Tests {
test := &s.Tests[i]
if err := test.RunTest(headers); err != nil {
return err
}
}
return nil
}
var SCOPES = []OpenAIScope{
{
Name: "Models",
Tests: []analyzers.HttpStatusTest{
{URL: BASE_URL + "/v1/models", Method: "GET", Valid: []int{200}, Invalid: []int{403}, Type: analyzers.READ, Status: analyzers.PermissionStatus{}},
},
Endpoints: []string{"/v1/models"},
},
{
Name: "Model capabilities",
Tests: []analyzers.HttpStatusTest{
{URL: BASE_URL + "/v1/images/generations", Method: "POST", Payload: POST_PAYLOAD, Valid: []int{400}, Invalid: []int{401}, Type: analyzers.WRITE, Status: analyzers.PermissionStatus{}},
},
Endpoints: []string{"/v1/audio", "/v1/chat/completions", "/v1/embeddings", "/v1/images", "/v1/moderations"},
},
{
Name: "Assistants",
Tests: []analyzers.HttpStatusTest{
{URL: BASE_URL + "/v1/assistants", Method: "GET", Valid: []int{400}, Invalid: []int{401}, Type: analyzers.READ, Status: analyzers.PermissionStatus{}},
{URL: BASE_URL + "/v1/assistants", Method: "POST", Payload: POST_PAYLOAD, Valid: []int{400}, Invalid: []int{401}, Type: analyzers.WRITE, Status: analyzers.PermissionStatus{}},
},
Endpoints: []string{"/v1/assistants"},
},
{
Name: "Threads",
Tests: []analyzers.HttpStatusTest{
{URL: BASE_URL + "/v1/threads/1", Method: "GET", Valid: []int{400}, Invalid: []int{401}, Type: analyzers.READ, Status: analyzers.PermissionStatus{}},
{URL: BASE_URL + "/v1/threads", Method: "POST", Payload: POST_PAYLOAD, Valid: []int{400}, Invalid: []int{401}, Type: analyzers.WRITE, Status: analyzers.PermissionStatus{}},
},
Endpoints: []string{"/v1/threads"},
},
{
Name: "Fine-tuning",
Tests: []analyzers.HttpStatusTest{
{URL: BASE_URL + "/v1/fine_tuning/jobs", Method: "GET", Valid: []int{200}, Invalid: []int{401}, Type: analyzers.READ, Status: analyzers.PermissionStatus{}},
{URL: BASE_URL + "/v1/fine_tuning/jobs", Method: "POST", Payload: POST_PAYLOAD, Valid: []int{400}, Invalid: []int{401}, Type: analyzers.WRITE, Status: analyzers.PermissionStatus{}},
},
Endpoints: []string{"/v1/fine_tuning"},
},
{
Name: "Files",
Tests: []analyzers.HttpStatusTest{
{URL: BASE_URL + "/v1/files", Method: "GET", Valid: []int{200}, Invalid: []int{401}, Type: analyzers.READ, Status: analyzers.PermissionStatus{}},
{URL: BASE_URL + "/v1/files", Method: "POST", Payload: POST_PAYLOAD, Valid: []int{415}, Invalid: []int{401}, Type: analyzers.WRITE, Status: analyzers.PermissionStatus{}},
},
Endpoints: []string{"/v1/files"},
},
}

View file

@ -0,0 +1,205 @@
package opsgenie
import (
"bytes"
_ "embed"
"encoding/json"
"errors"
"io"
"net/http"
"os"
"github.com/fatih/color"
"github.com/jedib0t/go-pretty/table"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/config"
)
//go:embed scopes.json
var scopesConfig []byte
type User struct {
FullName string `json:"fullName"`
Username string `json:"username"`
Role struct {
Name string `json:"name"`
} `json:"role"`
}
type UsersJSON struct {
Users []User `json:"data"`
}
type HttpStatusTest struct {
Endpoint string `json:"endpoint"`
Method string `json:"method"`
Payload interface{} `json:"payload"`
ValidStatuses []int `json:"valid_status_code"`
InvalidStatuses []int `json:"invalid_status_code"`
}
func StatusContains(status int, vals []int) bool {
for _, v := range vals {
if status == v {
return true
}
}
return false
}
func (h *HttpStatusTest) RunTest(cfg *config.Config, headers map[string]string) (bool, error) {
// If body data, marshal to JSON
var data io.Reader
if h.Payload != nil {
jsonData, err := json.Marshal(h.Payload)
if err != nil {
return false, err
}
data = bytes.NewBuffer(jsonData)
}
// Create new HTTP request
client := analyzers.NewAnalyzeClient(cfg)
req, err := http.NewRequest(h.Method, h.Endpoint, data)
if err != nil {
return false, err
}
// Add custom headers if provided
for key, value := range headers {
req.Header.Set(key, value)
}
// Execute HTTP Request
resp, err := client.Do(req)
if err != nil {
return false, err
}
defer resp.Body.Close()
// Check response status code
switch {
case StatusContains(resp.StatusCode, h.ValidStatuses):
return true, nil
case StatusContains(resp.StatusCode, h.InvalidStatuses):
return false, nil
default:
return false, errors.New("error checking response status code")
}
}
type Scope struct {
Name string `json:"name"`
HttpTest HttpStatusTest `json:"test"`
}
func readInScopes() ([]Scope, error) {
var scopes []Scope
if err := json.Unmarshal(scopesConfig, &scopes); err != nil {
return nil, err
}
return scopes, nil
}
func checkPermissions(cfg *config.Config, key string) []string {
scopes, err := readInScopes()
if err != nil {
color.Red("[x] Error reading in scopes: %s", err.Error())
return nil
}
permissions := make([]string, 0)
for _, scope := range scopes {
status, err := scope.HttpTest.RunTest(cfg, map[string]string{"Authorization": "GenieKey " + key})
if err != nil {
color.Red("[x] Error running test: %s", err.Error())
return nil
}
if status {
permissions = append(permissions, scope.Name)
}
}
return permissions
}
func contains(s []string, e string) bool {
for _, a := range s {
if a == e {
return true
}
}
return false
}
func getUserList(cfg *config.Config, key string) ([]User, error) {
// Create new HTTP request
client := analyzers.NewAnalyzeClient(cfg)
req, err := http.NewRequest("GET", "https://api.opsgenie.com/v2/users", nil)
if err != nil {
return nil, err
}
// Add custom headers if provided
req.Header.Set("Authorization", "GenieKey "+key)
// Execute HTTP Request
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
// Decode response body
var userList UsersJSON
err = json.NewDecoder(resp.Body).Decode(&userList)
if err != nil {
return nil, err
}
return userList.Users, nil
}
func AnalyzePermissions(cfg *config.Config, key string) {
permissions := checkPermissions(cfg, key)
if len(permissions) == 0 {
color.Red("[x] Invalid OpsGenie API key")
return
}
color.Green("[!] Valid OpsGenie API key\n\n")
printPermissions(permissions)
if contains(permissions, "Configuration Access") {
users, err := getUserList(cfg, key)
if err != nil {
color.Red("[x] Error getting user list: %s", err.Error())
return
}
printUsers(users)
}
color.Yellow("\n[i] Expires: Never")
}
func printPermissions(permissions []string) {
color.Yellow("[i] Permissions:")
t := table.NewWriter()
t.SetOutputMirror(os.Stdout)
t.AppendHeader(table.Row{"Permission"})
for _, permission := range permissions {
t.AppendRow(table.Row{color.GreenString(permission)})
}
t.Render()
}
func printUsers(users []User) {
color.Green("\n[i] Users:")
t := table.NewWriter()
t.SetOutputMirror(os.Stdout)
t.AppendHeader(table.Row{"Name", "Username", "Role"})
for _, user := range users {
t.AppendRow(table.Row{color.GreenString(user.FullName), color.GreenString(user.Username), color.GreenString(user.Role.Name)})
}
t.Render()
}

View file

@ -0,0 +1,38 @@
[
{
"name": "Configuration Access",
"test": {
"endpoint": "https://api.opsgenie.com/v2/account",
"method": "GET",
"valid_status_code": [200],
"invalid_status_code": [403]
}
},
{
"name": "Read",
"test": {
"endpoint": "https://api.opsgenie.com/v2/alerts",
"method": "GET",
"valid_status_code": [200],
"invalid_status_code": [403]
}
},
{
"name": "Delete",
"test": {
"endpoint": "https://api.opsgenie.com/v2/alerts/`nowaythiscanexist",
"method": "DELETE",
"valid_status_code": [202],
"invalid_status_code": [403]
}
},
{
"name": "Create and Update",
"test": {
"endpoint": "https://api.opsgenie.com/v2/alerts/`nowaycanthisexist/message",
"method": "PUT",
"valid_status_code": [400],
"invalid_status_code": [403]
}
}
]

View file

@ -0,0 +1,463 @@
package postgres
import (
"database/sql"
"errors"
"fmt"
"os"
"regexp"
"strings"
"github.com/fatih/color"
"github.com/jedib0t/go-pretty/table"
"github.com/lib/pq"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/config"
)
type DBPrivs struct {
Connect bool
Create bool
CreateTemp bool
}
type DB struct {
DatabaseName string
Owner string
DBPrivs
}
type TablePrivs struct {
Select bool
Insert bool
Update bool
Delete bool
Truncate bool
References bool
Trigger bool
}
type TableData struct {
Size string
Rows string
Privs TablePrivs
}
const (
pg_connect_timeout = "connect_timeout"
pg_dbname = "dbname"
pg_host = "host"
pg_password = "password"
pg_port = "port"
pg_requiressl = "requiressl"
pg_sslmode = "sslmode"
pg_sslmode_allow = "allow"
pg_sslmode_disable = "disable"
pg_sslmode_prefer = "prefer"
pg_sslmode_require = "require"
pg_user = "user"
)
var connStrPartPattern = regexp.MustCompile(`([[:alpha:]]+)='(.+?)' ?`)
func AnalyzePermissions(cfg *config.Config, connectionStr string) {
// ToDo: Add in logging
if cfg.LoggingEnabled {
color.Red("[x] Logging is not supported for this analyzer.")
return
}
connStr, err := pq.ParseURL(string(connectionStr))
if err != nil {
color.Red("[x] Failed to parse Postgres connection string.\n Error: " + err.Error())
return
}
parts := connStrPartPattern.FindAllStringSubmatch(connStr, -1)
params := make(map[string]string, len(parts))
for _, part := range parts {
params[part[1]] = part[2]
}
db, err := createConnection(params, "")
if err != nil {
color.Red("[x] Failed to connect to Postgres database.\n Error: " + err.Error())
return
}
defer db.Close()
color.Yellow("[!] Successfully connected to Postgres database.")
err = getUserPrivs(db)
if err != nil {
color.Red("[x] Failed to retrieve user privileges.\n Error: " + err.Error())
return
}
dbs, err := getDBPrivs(db)
if err != nil {
color.Red("[x] Failed to retrieve database privileges.\n Error: " + err.Error())
return
}
err = getTablePrivs(params, dbs)
if err != nil {
color.Red("[x] Failed to retrieve table privileges.\n Error: " + err.Error())
return
}
}
func isErrorDatabaseNotFound(err error, dbName string, user string) bool {
options := []string{dbName, user, "postgres"}
for _, option := range options {
if strings.Contains(err.Error(), fmt.Sprintf("database \"%s\" does not exist", option)) {
return true
}
}
return false
}
func createConnection(params map[string]string, database string) (*sql.DB, error) {
if sslmode := params[pg_sslmode]; sslmode == pg_sslmode_allow || sslmode == pg_sslmode_prefer {
// pq doesn't support 'allow' or 'prefer'. If we find either of them, we'll just ignore it. This will trigger
// the same logic that is run if no sslmode is set at all (which mimics 'prefer', which is the default).
delete(params, pg_sslmode)
}
var connStr string
for key, value := range params {
if database != "" && key == "dbname" {
connStr += fmt.Sprintf("%s='%s'", key, database)
} else {
connStr += fmt.Sprintf("%s='%s'", key, value)
}
}
db, err := sql.Open("postgres", connStr)
if err != nil {
return nil, err
}
err = db.Ping()
switch {
case err == nil:
return db, nil
case strings.Contains(err.Error(), "password authentication failed"):
return nil, errors.New("password authentication failed")
case errors.Is(err, pq.ErrSSLNotSupported) && params[pg_sslmode] == "":
// If the sslmode is unset, then either it was unset in the candidate secret, or we've intentionally unset it
// because it was specified as 'allow' or 'prefer', neither of which pq supports. In all of these cases, non-SSL
// connections are acceptable, so now we try a connection without SSL.
params[pg_sslmode] = pg_sslmode_disable
defer delete(params, pg_sslmode) // We want to return with the original params map intact (for ExtraData)
return createConnection(params, database)
case isErrorDatabaseNotFound(err, params[pg_dbname], params[pg_user]):
color.Green("[!] Successfully connected to Postgres database.")
return nil, err
default:
return nil, err
}
}
func getUserPrivs(db *sql.DB) error {
// Prepare the SQL statement
query := `SELECT rolname AS role_name,
rolsuper AS is_superuser,
rolinherit AS can_inherit,
rolcreaterole AS can_create_role,
rolcreatedb AS can_create_db,
rolcanlogin AS can_login,
rolreplication AS is_replication_role,
rolbypassrls AS bypasses_rls
FROM pg_roles WHERE rolname = current_user;`
// Execute the SQL query
rows, err := db.Query(query)
if err != nil {
return err
}
defer rows.Close()
var roleName string
var isSuperuser, canInherit, canCreateRole, canCreateDB, canLogin, isReplicationRole, bypassesRLS bool
// Iterate over the rows
for rows.Next() {
if err := rows.Scan(&roleName, &isSuperuser, &canInherit, &canCreateRole, &canCreateDB, &canLogin, &isReplicationRole, &bypassesRLS); err != nil {
return err
}
}
// Check for errors during iteration
if err := rows.Err(); err != nil {
return err
}
// Map roles to privileges
var mapRoles map[string]bool = map[string]bool{
"Superuser": isSuperuser,
"Inheritance of Privs": canInherit,
"Create Role": canCreateRole,
"Create DB": canCreateDB,
"Login": canLogin,
"Replication": isReplicationRole,
"Bypass RLS": bypassesRLS,
}
// Print User roles + privs
color.Yellow("[i] User: %s", roleName)
color.Yellow("[i] Privileges: ")
for role, priv := range mapRoles {
if role == "Superuser" && priv {
color.Green(" - %s", role)
} else if priv {
color.Yellow(" - %s", role)
}
}
return nil
}
func getDBPrivs(db *sql.DB) ([]string, error) {
query := `
SELECT
d.datname AS database_name,
u.usename AS owner,
current_user AS current_user,
has_database_privilege(current_user, d.datname, 'CONNECT') AS can_connect,
has_database_privilege(current_user, d.datname, 'CREATE') AS can_create,
has_database_privilege(current_user, d.datname, 'TEMP') AS can_create_temporary_tables
FROM
pg_database d
JOIN
pg_user u ON d.datdba = u.usesysid
WHERE
NOT d.datistemplate
ORDER BY
d.datname;
`
// Originally had WHERE NOT d.datistemplate AND d.datallowconn
// Execute the query
rows, err := db.Query(query)
if err != nil {
return nil, err
}
defer rows.Close()
dbs := make([]DB, 0)
var currentUser string
// Iterate through the result set
for rows.Next() {
var dbName, owner string
var canConnect, canCreate, canCreateTemp bool
err := rows.Scan(&dbName, &owner, &currentUser, &canConnect, &canCreate, &canCreateTemp)
if err != nil {
return nil, err
}
db := DB{
DatabaseName: dbName,
Owner: owner,
DBPrivs: DBPrivs{
Connect: canConnect,
Create: canCreate,
CreateTemp: canCreateTemp,
},
}
dbs = append(dbs, db)
}
if err = rows.Err(); err != nil {
return nil, err
}
// Print db privs
if len(dbs) > 0 {
fmt.Print("\n\n")
color.Green("[i] User has the following database privileges:")
printDBPrivs(dbs, currentUser)
return buildSliceDBNames(dbs), nil
}
return nil, nil
}
func printDBPrivs(dbs []DB, current_user string) {
t := table.NewWriter()
t.SetOutputMirror(os.Stdout)
t.AppendHeader(table.Row{"Database", "Owner", "Access Privileges"})
for _, db := range dbs {
privs := buildDBPrivsStr(db)
writer := getDBWriter(db, current_user)
t.AppendRow([]interface{}{writer(db.DatabaseName), writer(db.Owner), writer(privs)})
}
t.Render()
}
func buildDBPrivsStr(db DB) string {
privs := ""
if db.Connect {
privs += "CONNECT"
}
if db.Create {
privs += ", CREATE"
}
if db.CreateTemp {
privs += ", TEMP"
}
privs = strings.TrimPrefix(privs, ", ")
return privs
}
func getDBWriter(db DB, current_user string) func(a ...interface{}) string {
if db.Owner == current_user {
return analyzers.GreenWriter
} else if db.Connect && db.Create && db.CreateTemp {
return analyzers.GreenWriter
} else if db.Connect || db.Create || db.CreateTemp {
return analyzers.YellowWriter
} else {
return analyzers.DefaultWriter
}
}
func buildSliceDBNames(dbs []DB) []string {
var dbNames []string
for _, db := range dbs {
if db.DBPrivs.Connect {
dbNames = append(dbNames, db.DatabaseName)
}
}
return dbNames
}
func getTablePrivs(params map[string]string, databases []string) error {
tablePrivileges := make(map[string]map[string]*TableData, 0)
for _, dbase := range databases {
// Connect to db
db, err := createConnection(params, dbase)
if err != nil {
color.Red("[x] Failed to connect to Postgres database: %s", dbase)
continue
}
defer db.Close()
// Get table privs
query := `
SELECT
rtg.table_catalog,
rtg.table_name,
rtg.privilege_type,
pg_size_pretty(pg_total_relation_size(pc.oid)) AS table_size,
pc.reltuples AS estimate
FROM
information_schema.role_table_grants rtg
JOIN
pg_catalog.pg_class pc ON rtg.table_name = pc.relname
WHERE
rtg.grantee = current_user;
`
// Execute the query
rows, err := db.Query(query)
if err != nil {
return err
}
defer rows.Close()
// Iterate through the result set
for rows.Next() {
var database, table, priv, size, row_count string
err := rows.Scan(&database, &table, &priv, &size, &row_count)
if err != nil {
return err
}
if _, ok := tablePrivileges[database]; !ok {
tablePrivileges[database] = map[string]*TableData{
table: {},
}
}
switch priv {
case "SELECT":
tablePrivileges[database][table].Privs.Select = true
case "INSERT":
tablePrivileges[database][table].Privs.Insert = true
case "UPDATE":
tablePrivileges[database][table].Privs.Update = true
case "DELETE":
tablePrivileges[database][table].Privs.Delete = true
case "TRUNCATE":
tablePrivileges[database][table].Privs.Truncate = true
case "REFERENCES":
tablePrivileges[database][table].Privs.References = true
case "TRIGGER":
tablePrivileges[database][table].Privs.Trigger = true
}
tablePrivileges[database][table].Size = size
if row_count != "-1" {
tablePrivileges[database][table].Rows = row_count
} else {
tablePrivileges[database][table].Rows = "Unknown"
}
}
if err = rows.Err(); err != nil {
return err
}
db.Close()
}
// Print table privs
if len(tablePrivileges) > 0 {
fmt.Print("\n\n")
color.Green("[i] User has the following table privileges:")
printTablePrivs(tablePrivileges)
}
return nil
}
func printTablePrivs(tables map[string]map[string]*TableData) {
t := table.NewWriter()
t.SetOutputMirror(os.Stdout)
t.AppendHeader(table.Row{"Database", "Table", "Access Privileges", "Est. Size", "Est. Rows"})
var writer func(a ...interface{}) string
for db, table := range tables {
for table_name, tableData := range table {
privs := tableData.Privs
privsStr := buildTablePrivsStr(privs)
if privsStr == "" {
writer = color.New().SprintFunc()
} else {
writer = color.New(color.FgGreen).SprintFunc()
}
t.AppendRow([]interface{}{writer(db), writer(table_name), writer(privsStr), writer("< " + tableData.Size), writer(tableData.Rows)})
}
}
t.Render()
}
func buildTablePrivsStr(privs TablePrivs) string {
var privsStr string
if privs.Select {
privsStr += "SELECT"
}
if privs.Insert {
privsStr += ", INSERT"
}
if privs.Update {
privsStr += ", UPDATE"
}
if privs.Delete {
privsStr += ", DELETE"
}
if privs.Truncate {
privsStr += ", TRUNCATE"
}
if privs.References {
privsStr += ", REFERENCES"
}
if privs.Trigger {
privsStr += ", TRIGGER"
}
privsStr = strings.TrimPrefix(privsStr, ", ")
return privsStr
}

View file

@ -0,0 +1,152 @@
package postman
import (
"encoding/json"
"fmt"
"net/http"
"os"
"github.com/fatih/color"
"github.com/jedib0t/go-pretty/table"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/config"
)
type UserInfoJSON struct {
User struct {
Username string `json:"username"`
Email string `json:"email"`
FullName string `json:"fullName"`
Roles []string `json:"roles"`
TeamName string `json:"teamName"`
TeamDomain string `json:"teamDomain"`
} `json:"user"`
}
type WorkspaceJSON struct {
Workspaces []struct {
ID string `json:"id"`
Name string `json:"name"`
Type string `json:"type"`
Visibility string `json:"visibility"`
} `json:"workspaces"`
}
func getUserInfo(cfg *config.Config, key string) (UserInfoJSON, error) {
var me UserInfoJSON
client := analyzers.NewAnalyzeClient(cfg)
req, err := http.NewRequest("GET", "https://api.getpostman.com/me", nil)
if err != nil {
return me, err
}
req.Header.Add("X-API-Key", key)
// send request
resp, err := client.Do(req)
if err != nil {
return me, err
}
// read response
defer resp.Body.Close()
// if status code is 200, decode response
if resp.StatusCode == 200 {
err = json.NewDecoder(resp.Body).Decode(&me)
}
return me, err
}
func getWorkspaces(cfg *config.Config, key string) (WorkspaceJSON, error) {
var workspaces WorkspaceJSON
client := analyzers.NewAnalyzeClient(cfg)
req, err := http.NewRequest("GET", "https://api.getpostman.com/workspaces", nil)
if err != nil {
return workspaces, err
}
req.Header.Add("X-API-Key", key)
// send request
resp, err := client.Do(req)
if err != nil {
return workspaces, err
}
// read response
defer resp.Body.Close()
// if status code is 200, decode response
if resp.StatusCode == 200 {
err = json.NewDecoder(resp.Body).Decode(&workspaces)
}
return workspaces, err
}
func AnalyzePermissions(cfg *config.Config, key string) {
// validate key & get user info
me, err := getUserInfo(cfg, key)
if err != nil {
color.Red("[x]" + err.Error())
}
if me.User.Username == "" {
color.Red("[x] Invalid Postman API Key")
return
}
// print user info
printUserInfo(me)
// get workspaces
workspaces, err := getWorkspaces(cfg, key)
if err != nil {
color.Red("[x]" + err.Error())
}
if len(workspaces.Workspaces) == 0 {
color.Red("[x] No Workspaces Found")
return
}
// print workspaces
printWorkspaces(workspaces)
}
func printUserInfo(me UserInfoJSON) {
color.Green("[!] Valid Postman API Key")
color.Yellow("\n[i] User Information")
color.Green("Username: " + me.User.Username)
color.Green("Email: " + me.User.Email)
color.Green("Full Name: " + me.User.FullName)
color.Yellow("\n[i] Team Information")
color.Green("Name: " + me.User.TeamName)
color.Green("Domain: https://" + me.User.TeamDomain + ".postman.co")
t := table.NewWriter()
t.SetOutputMirror(os.Stdout)
t.AppendHeader(table.Row{"Scope", "Permissions"})
for _, role := range me.User.Roles {
t.AppendRow([]interface{}{color.GreenString(role), color.GreenString(roleDescriptions[role])})
}
t.Render()
fmt.Println("Reference: https://learning.postman.com/docs/collaborating-in-postman/roles-and-permissions/#team-roles")
}
func printWorkspaces(workspaces WorkspaceJSON) {
color.Yellow("[i] Accessible Workspaces")
t := table.NewWriter()
t.SetOutputMirror(os.Stdout)
t.AppendHeader(table.Row{"Workspace Name", "Type", "Visibility", "Link"})
for _, workspace := range workspaces.Workspaces {
t.AppendRow([]interface{}{color.GreenString(workspace.Name), color.GreenString(workspace.Type), color.GreenString(workspace.Visibility), color.GreenString("https://go.postman.co/workspaces/" + workspace.ID)})
}
t.Render()
}

View file

@ -0,0 +1,13 @@
package postman
var roleDescriptions = map[string]string{
"super-admin": "(Enterprise Only) Manages everything within a team, including team settings, members, roles, and resources. This role can view and manage all elements in public, team, private, and personal workspaces. Super Admins can perform all actions that other roles can perform.",
"admin": "Manages team members and team settings. Can also view monitor metadata and run, pause, and resume monitors.",
"billing": "Manages team plan and payments. Billing roles can be granted by a Super Admin, Team Admin, or by a fellow team member with a Billing role.",
"user": "Has access to all team resources and workspaces.",
"community-manager": "(Pro & Enterprise Only) Manages the public visibility of workspaces and team profile.",
"partner-manager": "(Internal, Enterprise plans only) - Manages all Partner Workspaces within an organization. Controls Partner Workspace settings and visibility, and can send invites to partners.",
"partner": "(External, Professional and Enterprise plans only) - All partners are automatically granted the Partner role at the team level. Partners can only access the Partner Workspaces they've been invited to.",
"guest": "Views collections and sends requests in collections that have been shared with them. This role can't be directly assigned to a user.",
"flow-editor": "(Basic and Professional plans only) - Can create, edit, run, and publish Postman Flows.",
}

View file

@ -0,0 +1,105 @@
package sendgrid
import (
"strings"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers"
)
type SendgridScope struct {
Category string
SubCategory string
Prefixes []string // Prefixes for the scope
Permissions []string
PermissionType analyzers.PermissionType
}
func (s *SendgridScope) AddPermission(permission string) {
s.Permissions = append(s.Permissions, permission)
}
func (s *SendgridScope) RunTests() {
if len(s.Permissions) == 0 {
s.PermissionType = analyzers.NONE
return
}
for _, permission := range s.Permissions {
if strings.Contains(permission, ".read") {
s.PermissionType = analyzers.READ
} else {
s.PermissionType = analyzers.READ_WRITE
return
}
}
}
var SCOPES = []SendgridScope{
// Billing
{Category: "Billing", Prefixes: []string{"billing"}},
// Restricted Access
{Category: "API Keys", Prefixes: []string{"api_keys"}},
{Category: "Alerts", Prefixes: []string{"alerts"}},
{Category: "Category Management", Prefixes: []string{"categories"}},
{Category: "Design Library", Prefixes: []string{"design_library"}},
{Category: "Email Activity", Prefixes: []string{"messages"}},
{Category: "Email Testing", Prefixes: []string{"email_testing"}},
{Category: "IP Management", Prefixes: []string{"ips"}},
{Category: "Inbound Parse", Prefixes: []string{"user.webhooks.parse.settings"}},
{Category: "Mail Send", SubCategory: "Mail Send", Prefixes: []string{"mail.send"}},
{Category: "Mail Send", SubCategory: "Scheduled Sends", Prefixes: []string{"user.scheduled_sends, mail.batch"}},
{Category: "Mail Settings", SubCategory: "Address Allow List", Prefixes: []string{"mail_settings.address_whitelist"}},
{Category: "Mail Settings", SubCategory: "BCC", Prefixes: []string{"mail_settings.bcc"}},
{Category: "Mail Settings", SubCategory: "Bounce Purge", Prefixes: []string{"mail_settings.bounce_purge"}},
{Category: "Mail Settings", SubCategory: "Event Notification", Prefixes: []string{"user.webhooks.event"}},
{Category: "Mail Settings", SubCategory: "Footer", Prefixes: []string{"mail_settings.footer"}},
{Category: "Mail Settings", SubCategory: "Forward Bounce", Prefixes: []string{"mail_settings.forward_bounce"}},
{Category: "Mail Settings", SubCategory: "Forward Spam", Prefixes: []string{"mail_settings.forward_spam"}},
{Category: "Mail Settings", SubCategory: "Legacy Email Template", Prefixes: []string{"mail_settings.template"}},
{Category: "Mail Settings", SubCategory: "Plain Content", Prefixes: []string{"mail_settings.plain_content"}},
{Category: "Mail Settings", SubCategory: "Spam Checker", Prefixes: []string{"mail_settings.spam_check"}},
{Category: "Marketing", SubCategory: "Automation", Prefixes: []string{"marketing.automation"}},
{Category: "Marketing", SubCategory: "Marketing", Prefixes: []string{"marketing.read"}},
{Category: "Partners", Prefixes: []string{"partner_settings"}},
{Category: "Recipients Data Erasure", Prefixes: []string{"recipients"}},
{Category: "Security", Prefixes: []string{"access_settings"}},
{Category: "Sender Authentication", Prefixes: []string{"whitelabel"}},
{Category: "Stats", SubCategory: "Browser Stats", Prefixes: []string{"browsers"}},
{Category: "Stats", SubCategory: "Category Stats", Prefixes: []string{"categories.stats"}},
{Category: "Stats", SubCategory: "Email Clients and Devices", Prefixes: []string{"clients", "devices"}},
{Category: "Stats", SubCategory: "Geographical", Prefixes: []string{"geo"}},
{Category: "Stats", SubCategory: "Global Stats", Prefixes: []string{"stats.global"}},
{Category: "Stats", SubCategory: "Mailbox Provider Stats", Prefixes: []string{"mailbox_providers"}},
{Category: "Stats", SubCategory: "Parse Webhook", Prefixes: []string{"user.webhooks.parse.stats"}},
{Category: "Stats", SubCategory: "Stats Overview", Prefixes: []string{"stats.read"}},
{Category: "Stats", SubCategory: "Subuser Stats", Prefixes: []string{"subusers"}},
{Category: "Suppressions", SubCategory: "Supressions", Prefixes: []string{"suppression"}},
{Category: "Suppressions", SubCategory: "Unsubscribe Groups", Prefixes: []string{"asm.groups"}},
{Category: "Template Engine", Prefixes: []string{"templates"}},
{Category: "Tracking", SubCategory: "Click Tracking", Prefixes: []string{"tracking_settings.click"}},
{Category: "Tracking", SubCategory: "Google Analytics", Prefixes: []string{"tracking_settings.google_analytics"}},
{Category: "Tracking", SubCategory: "Open Tracking", Prefixes: []string{"tracking_settings.open"}},
{Category: "Tracking", SubCategory: "Subscription Tracking", Prefixes: []string{"tracking_settings.subscription"}},
{Category: "User Account", SubCategory: "Enforced TLS", Prefixes: []string{"user.settings.enforced_tls"}},
{Category: "User Account", SubCategory: "Timezone", Prefixes: []string{"user.timezone"}},
// Full Access Additional Categories
{Category: "Suppressions", SubCategory: "Unsubscribe Group Suppressions", Prefixes: []string{"asm.groups.suppressions"}},
{Category: "Suppressions", SubCategory: "Global Suppressions", Prefixes: []string{"asm.suppressions.global"}},
{Category: "Credentials", Prefixes: []string{"credentials"}},
{Category: "Mail Settings", Prefixes: []string{"mail_settings"}},
{Category: "Signup", Prefixes: []string{"signup"}},
{Category: "Suppressions", SubCategory: "Blocks", Prefixes: []string{"suppression.blocks"}},
{Category: "Suppressions", SubCategory: "Bounces", Prefixes: []string{"suppression.bounces"}},
{Category: "Suppressions", SubCategory: "Invalid Emails", Prefixes: []string{"suppression.invalid_emails"}},
{Category: "Suppressions", SubCategory: "Spam Reports", Prefixes: []string{"suppression.spam_reports"}},
{Category: "Suppressions", SubCategory: "Unsubscribes", Prefixes: []string{"suppression.unsubscribes"}},
{Category: "Teammates", Prefixes: []string{"teammates"}},
{Category: "Tracking", Prefixes: []string{"tracking_settings"}},
{Category: "UI", Prefixes: []string{"ui"}},
{Category: "User Account", SubCategory: "Account", Prefixes: []string{"user.account"}},
{Category: "User Account", SubCategory: "Credits", Prefixes: []string{"user.credits"}},
{Category: "User Account", SubCategory: "Email", Prefixes: []string{"user.email"}},
{Category: "User Account", SubCategory: "Multifactor Authentication", Prefixes: []string{"user.multifactor_authentication"}},
{Category: "User Account", SubCategory: "Password", Prefixes: []string{"user.password"}},
{Category: "User Account", SubCategory: "Profile", Prefixes: []string{"user.profile"}},
{Category: "User Account", SubCategory: "Username", Prefixes: []string{"user.username"}},
}

View file

@ -0,0 +1,133 @@
package sendgrid
import (
"encoding/json"
"fmt"
"os"
"slices"
"strings"
"github.com/fatih/color"
"github.com/jedib0t/go-pretty/v6/table"
sg "github.com/sendgrid/sendgrid-go"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/config"
)
type ScopesJSON struct {
Scopes []string `json:"scopes"`
}
func printPermissions(show_all bool) {
fmt.Print("\n\n")
t := table.NewWriter()
t.SetOutputMirror(os.Stdout)
if show_all {
t.AppendHeader(table.Row{"Scope", "Sub-Scope", "Access", "Permissions"})
} else {
t.AppendHeader(table.Row{"Scope", "Sub-Scope", "Access"})
}
// Print the scopes
for _, s := range SCOPES {
writer := analyzers.GetWriterFromStatus(s.PermissionType)
if show_all {
t.AppendRow([]interface{}{writer(s.Category), writer(s.SubCategory), writer(s.PermissionType), writer(strings.Join(s.Permissions, "\n"))})
} else if s.PermissionType != analyzers.NONE {
t.AppendRow([]interface{}{writer(s.Category), writer(s.SubCategory), writer(s.PermissionType)})
}
}
t.Render()
fmt.Print("\n\n")
}
// getCategoryFromScope returns the category for a given scope.
// It will return the most specific category possible.
// For example, if the scope is "mail.send.read", it will return "Mail Send", not just "Mail"
// since it's searching "mail.send.read" -> "mail.send" -> "mail"
func getScopeIndex(scope string) int {
splitScope := strings.Split(scope, ".")
for i := len(splitScope); i > 0; i-- {
searchScope := strings.Join(splitScope[:i], ".")
for i, s := range SCOPES {
for _, prefix := range s.Prefixes {
if strings.HasPrefix(searchScope, prefix) {
return i
}
}
}
}
return -1
}
func processPermissions(rawScopes []string) {
for _, scope := range rawScopes {
// Skip these scopes since they are not useful for this analysis
if scope == "2fa_required" || scope == "sender_verification_eligible" {
continue
}
ind := getScopeIndex(scope)
if ind == -1 {
//color.Red("[!] Scope not found: %v", scope)
continue
}
s := &SCOPES[ind]
s.AddPermission(scope)
}
// Run tests to determine the permission type
for i := range SCOPES {
SCOPES[i].RunTests()
}
}
func AnalyzePermissions(cfg *config.Config, key string) {
// ToDo: Add logging when rewrite to not use SG client.
if cfg.LoggingEnabled {
color.Red("[x] Logging not supported for GitHub Token Analysis.")
return
}
req := sg.GetRequest(key, "/v3/scopes", "https://api.sendgrid.com")
req.Method = "GET"
resp, err := sg.API(req)
if resp.StatusCode == 401 || resp.StatusCode == 403 {
color.Red("[!] Invalid API Key")
return
} else if resp.StatusCode != 200 {
color.Red("[!] Error: %v", resp.StatusCode)
return
}
if err != nil {
color.Red("[!] Error: %v", err)
return
}
color.Green("[!] Valid Sendgrid API Key\n\n")
// Unmarshal the JSON response into a struct
var jsonScopes ScopesJSON
if err := json.Unmarshal([]byte(resp.Body), &jsonScopes); err != nil {
color.Red("Error:", err)
return
}
// Now you can access the scopes
rawScopes := jsonScopes.Scopes
if slices.Contains(rawScopes, "user.email.read") {
color.Green("[*] Sendgrid Key Type: Full Access Key")
} else if slices.Contains(rawScopes, "billing.read") {
color.Yellow("[*] Sendgrid Key Type: Billing Access Key")
} else {
color.Yellow("[*] Sendgrid Key Type: Restricted Access Key")
}
if slices.Contains(rawScopes, "2fa_required") {
color.Yellow("[i] 2FA Required for this account")
}
processPermissions(rawScopes)
printPermissions(cfg.ShowAll)
}

View file

@ -0,0 +1,470 @@
{
"categories": {
"Analytics": {
"description": "View store metrics",
"scopes": {
"read_analytics": "Read"
}
},
"Applications": {
"description": "View or manage apps",
"scopes": {
"read_apps": "Read"
}
},
"Assigned fulfillment orders": {
"description": "View or manage fulfillment orders",
"scopes": {
"write_assigned_fulfillment_orders": "Write",
"read_assigned_fulfillment_orders": "Read"
}
},
"Browsing behavior": {
"description": "View or manage online-store browsing behavior including page views, cart updates, product views and searches",
"scopes": {
"read_customer_events": "Read"
}
},
"Custom pixels": {
"description": "View or manage custom pixels",
"scopes": {
"write_custom_pixels": "Write",
"read_custom_pixels": "Read"
}
},
"Customers": {
"description": "View or manage customers, customer addresses, order history, and customer groups",
"scopes": {
"write_customers": "Write",
"read_customers": "Read"
}
},
"Discounts": {
"description": "View or manage automatic discounts and discount codes",
"scopes": {
"write_discounts": "Write",
"read_discounts": "Read"
}
},
"Discovery": {
"description": "View or manage Discovery API",
"scopes": {
"write_discovery": "Write",
"read_discovery": "Read"
}
},
"Draft orders": {
"description": "View or manage orders created by merchants on behalf of customers",
"scopes": {
"write_draft_orders": "Write",
"read_draft_orders": "Read"
}
},
"Files": {
"description": "View or manage files",
"scopes": {
"write_files": "Write",
"read_files": "Read"
}
},
"Fulfillment services": {
"description": "View or manage fulfillment services",
"scopes": {
"write_fulfillments": "Write",
"read_fulfillments": "Read"
}
},
"Gift cards": {
"description": "View or manage gift cards",
"scopes": {
"write_gift_cards": "Write",
"read_gift_cards": "Read"
}
},
"Inventory": {
"description": "View or manage inventory across multiple locations",
"scopes": {
"write_inventory": "Write",
"read_inventory": "Read"
}
},
"Legal policies": {
"description": "View or manage a shop's legal policies",
"scopes": {
"write_legal_policies": "Write",
"read_legal_policies": "Read"
}
},
"Locations": {
"description": "View the geographic location of stores, headquarters, and warehouses",
"scopes": {
"write_locations": "Write",
"read_locations": "Read"
}
},
"Marketing events": {
"description": "View or manage marketing events and engagement data",
"scopes": {
"write_marketing_events": "Write",
"read_marketing_events": "Read"
}
},
"Merchant-managed fulfillment orders": {
"description": "View or manage fulfillment orders assigned to merchant-managed locations",
"scopes": {
"write_merchant_managed_fulfillment_orders": "Write",
"read_merchant_managed_fulfillment_orders": "Read"
}
},
"Metaobject definitions": {
"description": "View or manage definitions",
"scopes": {
"write_metaobject_definitions": "Write",
"read_metaobject_definitions": "Read"
}
},
"Metaobject entries": {
"description": "View or manage entries",
"scopes": {
"write_metaobjects": "Write",
"read_metaobjects": "Read"
}
},
"Online Store navigation": {
"description": "View menus for display on the storefront",
"scopes": {
"write_online_store_navigation": "Write",
"read_online_store_navigation": "Read"
}
},
"Online Store pages": {
"description": "View or manage Online Store pages",
"scopes": {
"write_online_store_pages": "Write",
"read_online_store_pages": "Read"
}
},
"Order editing": {
"description": "View or manage edits to orders",
"scopes": {
"write_order_edits": "Write",
"read_order_edits": "Read"
}
},
"Orders": {
"description": "View or manage orders, transactions, fulfillments, and abandoned checkouts",
"scopes": {
"write_orders": "Write",
"read_orders": "Read"
}
},
"Packing slip management": {
"description": "Edit and preview packing slip template",
"scopes": {
"write_packing_slip_templates": "Write",
"read_packing_slip_templates": "Read"
}
},
"Payment customizations": {
"description": "View or manage payment customizations",
"scopes": {
"write_payment_customizations": "Write",
"read_payment_customizations": "Read"
}
},
"Payment terms": {
"description": "View or manage payment terms",
"scopes": {
"write_payment_terms": "Write",
"read_payment_terms": "Read"
}
},
"Pixels": {
"description": "View or manage pixels",
"scopes": {
"write_pixels": "Write",
"read_pixels": "Read"
}
},
"Price rules": {
"description": "View or manage conditional discounts",
"scopes": {
"write_price_rules": "Write",
"read_price_rules": "Read"
}
},
"Product feeds": {
"description": "View or manage product feeds",
"scopes": {
"write_product_feeds": "Write",
"read_product_feeds": "Read"
}
},
"Product listings": {
"description": "View or manage product or collection listings",
"scopes": {
"write_product_listings": "Write",
"read_product_listings": "Read"
}
},
"Products": {
"description": "View or manage products, variants, and collections",
"scopes": {
"write_products": "Write",
"read_products": "Read"
}
},
"Publications": {
"description": "View or manage groups of products that have been published to an app",
"scopes": {
"write_publications": "Write",
"read_publications": "Read"
}
},
"Purchase options": {
"description": "View or manage purchase options owned by this app",
"scopes": {
"write_purchase_options": "Write",
"read_purchase_options": "Read"
}
},
"Reports": {
"description": "View or manage reports on the Reports page in the Shopify admin",
"scopes": {
"write_reports": "Write",
"read_reports": "Read"
}
},
"Resource feedback": {
"description": "View or manage the status of shops and resources",
"scopes": {
"write_resource_feedbacks": "Write",
"read_resource_feedbacks": "Read"
}
},
"Returns": {
"description": "View or manage returns",
"scopes": {
"write_returns": "Write",
"read_returns": "Read"
}
},
"Sales channels": {
"description": "View or manage sales channels",
"scopes": {
"write_channels": "Write",
"read_channels": "Read"
}
},
"Script tags": {
"description": "View or manage the JavaScript code in storefront or orders status pages",
"scopes": {
"write_script_tags": "Write",
"read_script_tags": "Read"
}
},
"Shipping": {
"description": "View or manage shipping carriers, countries, and provinces",
"scopes": {
"write_shipping": "Write",
"read_shipping": "Read"
}
},
"Shop locales": {
"description": "View or manage available locales for a shop",
"scopes": {
"write_locales": "Write",
"read_locales": "Read"
}
},
"Shopify Markets": {
"description": "View or manage Shopify Markets configuration",
"scopes": {
"write_markets": "Write",
"read_markets": "Read"
}
},
"Shopify Payments accounts": {
"description": "View Shopify Payments accounts",
"scopes": {
"read_shopify_payments_accounts": "Read"
}
},
"Shopify Payments bank accounts": {
"description": "View bank accounts that can receive Shopify Payment payouts",
"scopes": {
"read_shopify_payments_bank_accounts": "Read"
}
},
"Shopify Payments disputes": {
"description": "View Shopify Payment disputes raised by buyers",
"scopes": {
"write_shopify_payments_disputes": "Write",
"read_shopify_payments_disputes": "Read"
}
},
"Shopify Payments payouts": {
"description": "View Shopify Payments payouts and the account's current balance",
"scopes": {
"read_shopify_payments_payouts": "Read"
}
},
"Store content": {
"description": "View or manage articles, blogs, comments, pages, and redirects",
"scopes": {
"write_content": "Write",
"read_content": "Read"
}
},
"Store credit account transactions": {
"description": "View or create store credit transactions",
"scopes": {
"write_store_credit_account_transactions": "Write",
"read_store_credit_account_transactions": "Read"
}
},
"Store credit accounts": {
"description": "View a customer's store credit balance and currency",
"scopes": {
"read_store_credit_accounts": "Read"
}
},
"Themes": {
"description": "View or manage theme templates and assets",
"scopes": {
"write_themes": "Write",
"read_themes": "Read"
}
},
"Third-party fulfillment orders": {
"description": "View or manage fulfillment orders assigned to a location managed by any fulfillment service",
"scopes": {
"write_third_party_fulfillment_orders": "Write",
"read_third_party_fulfillment_orders": "Read"
}
},
"Translations": {
"description": "View or manage content that can be translated",
"scopes": {
"write_translations": "Write",
"read_translations": "Read"
}
},
"all_cart_transforms": {
"description": "",
"scopes": {
"read_all_cart_transforms": "Read"
}
},
"all_checkout_completion_target_customizations": {
"description": "",
"scopes": {
"write_all_checkout_completion_target_customizations": "Write",
"read_all_checkout_completion_target_customizations": "Read"
}
},
"cart_transforms": {
"description": "",
"scopes": {
"write_cart_transforms": "Write",
"read_cart_transforms": "Read"
}
},
"cash_tracking": {
"description": "",
"scopes": {
"read_cash_tracking": "Read"
}
},
"companies": {
"description": "",
"scopes": {
"write_companies": "Write",
"read_companies": "Read"
}
},
"custom_fulfillment_services": {
"description": "",
"scopes": {
"write_custom_fulfillment_services": "Write",
"read_custom_fulfillment_services": "Read"
}
},
"customer_data_erasure": {
"description": "",
"scopes": {
"write_customer_data_erasure": "Write",
"read_customer_data_erasure": "Read"
}
},
"customer_merge": {
"description": "",
"scopes": {
"write_customer_merge": "Write",
"read_customer_merge": "Read"
}
},
"delivery_customizations": {
"description": "",
"scopes": {
"write_delivery_customizations": "Write",
"read_delivery_customizations": "Read"
}
},
"delivery_option_generators": {
"description": "",
"scopes": {
"write_delivery_option_generators": "Write",
"read_delivery_option_generators": "Read"
}
},
"discounts_allocator_functions": {
"description": "",
"scopes": {
"write_discounts_allocator_functions": "Write",
"read_discounts_allocator_functions": "Read"
}
},
"fulfillment_constraint_rules": {
"description": "",
"scopes": {
"write_fulfillment_constraint_rules": "Write",
"read_fulfillment_constraint_rules": "Read"
}
},
"gates": {
"description": "",
"scopes": {
"write_gates": "Write",
"read_gates": "Read"
}
},
"order_submission_rules": {
"description": "",
"scopes": {
"write_order_submission_rules": "Write",
"read_order_submission_rules": "Read"
}
},
"privacy_settings": {
"description": "",
"scopes": {
"write_privacy_settings": "Write",
"read_privacy_settings": "Read"
}
},
"shopify_payments_provider_accounts_sensitive": {
"description": "",
"scopes": {
"read_shopify_payments_provider_accounts_sensitive": "Read"
}
},
"validations": {
"description": "",
"scopes": {
"write_validations": "Write",
"read_validations": "Read"
}
}
}
}

View file

@ -0,0 +1,213 @@
package shopify
import (
_ "embed"
"encoding/json"
"fmt"
"net/http"
"os"
"strings"
"github.com/fatih/color"
"github.com/jedib0t/go-pretty/table"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/config"
)
//go:embed scopes.json
var scopesConfig []byte
func sliceContains(slice []string, value string) bool {
for _, v := range slice {
if v == value {
return true
}
}
return false
}
type OutputScopes struct {
Description string
Scopes []string
}
func (o OutputScopes) PrintScopes() string {
// Custom rules unique to this analyzer
var scopes []string
if sliceContains(o.Scopes, "Read") && sliceContains(o.Scopes, "Write") {
scopes = append(scopes, "Read & Write")
for _, scope := range o.Scopes {
if scope != "Read" && scope != "Write" {
scopes = append(scopes, scope)
}
}
} else {
scopes = append(scopes, o.Scopes...)
}
return strings.Join(scopes, ", ")
}
// Category represents the structure of each category in the JSON
type CategoryJSON struct {
Description string `json:"description"`
Scopes map[string]string `json:"scopes"`
}
// Data represents the overall JSON structure
type ScopeDataJSON struct {
Categories map[string]CategoryJSON `json:"categories"`
}
// Function to determine the appropriate scope
func determineScopes(data ScopeDataJSON, input string) map[string]OutputScopes {
// Split the input string into individual scopes
inputScopes := strings.Split(input, ", ")
// Map to store scopes found for each category
scopeResults := make(map[string]OutputScopes)
// Populate categoryScopes map with individual scopes found
for _, scope := range inputScopes {
for category, catData := range data.Categories {
if scopeType, exists := catData.Scopes[scope]; exists {
if _, ok := scopeResults[category]; !ok {
scopeResults[category] = OutputScopes{Description: catData.Description}
}
// Extract the struct from the map
outputData := scopeResults[category]
// Modify the struct (ex: append "Read" or "Write" to the Scopes slice)
outputData.Scopes = append(outputData.Scopes, scopeType)
// Reassign the modified struct back to the map
scopeResults[category] = outputData
}
}
}
return scopeResults
}
type ShopInfoJSON struct {
Shop struct {
Name string `json:"name"`
Email string `json:"email"`
CreatedAt string `json:"created_at"`
} `json:"shop"`
}
func getShopInfo(cfg *config.Config, key string, store string) (ShopInfoJSON, error) {
var shopInfo ShopInfoJSON
client := analyzers.NewAnalyzeClient(cfg)
req, err := http.NewRequest("GET", fmt.Sprintf("https://%s/admin/api/2024-04/shop.json", store), nil)
if err != nil {
return shopInfo, err
}
req.Header.Set("X-Shopify-Access-Token", key)
resp, err := client.Do(req)
if err != nil {
return shopInfo, err
}
defer resp.Body.Close()
err = json.NewDecoder(resp.Body).Decode(&shopInfo)
if err != nil {
return shopInfo, err
}
return shopInfo, nil
}
type AccessScopesJSON struct {
AccessScopes []struct {
Handle string `json:"handle"`
} `json:"access_scopes"`
}
func (a AccessScopesJSON) String() string {
var scopes []string
for _, scope := range a.AccessScopes {
scopes = append(scopes, scope.Handle)
}
return strings.Join(scopes, ", ")
}
func getAccessScopes(cfg *config.Config, key string, store string) (AccessScopesJSON, int, error) {
var accessScopes AccessScopesJSON
client := analyzers.NewAnalyzeClient(cfg)
req, err := http.NewRequest("GET", fmt.Sprintf("https://%s/admin/oauth/access_scopes.json", store), nil)
if err != nil {
return accessScopes, -1, err
}
req.Header.Set("X-Shopify-Access-Token", key)
resp, err := client.Do(req)
if err != nil {
return accessScopes, resp.StatusCode, err
}
defer resp.Body.Close()
err = json.NewDecoder(resp.Body).Decode(&accessScopes)
if err != nil {
return accessScopes, resp.StatusCode, err
}
return accessScopes, resp.StatusCode, nil
}
func AnalyzePermissions(cfg *config.Config, key string, storeURL string) {
accessScopes, statusCode, err := getAccessScopes(cfg, key, storeURL)
if err != nil {
color.Red("Error: %s", err)
return
}
if statusCode != 200 {
color.Red("[x] Invalid Shopfiy API Key and Store URL combination")
return
}
color.Green("[i] Valid Shopify API Key\n\n")
shopInfo, err := getShopInfo(cfg, key, storeURL)
if err != nil {
color.Red("Error: %s", err)
return
}
color.Yellow("[i] Shop Information\n")
color.Yellow("Name: %s", shopInfo.Shop.Name)
color.Yellow("Email: %s", shopInfo.Shop.Email)
color.Yellow("Created At: %s\n\n", shopInfo.Shop.CreatedAt)
var data ScopeDataJSON
if err := json.Unmarshal(scopesConfig, &data); err != nil {
color.Red("Error: %s", err)
return
}
scopes := determineScopes(data, accessScopes.String())
printAccessScopes(scopes)
}
func printAccessScopes(accessScopes map[string]OutputScopes) {
color.Yellow("[i] Access Scopes\n")
t := table.NewWriter()
t.SetOutputMirror(os.Stdout)
t.AppendHeader(table.Row{"Scope", "Description", "Access"})
// order the categories
categoryOrder := []string{"Analytics", "Applications", "Assigned fulfillment orders", "Browsing behavior", "Custom pixels", "Customers", "Discounts", "Discovery", "Draft orders", "Files", "Fulfillment services", "Gift cards", "Inventory", "Legal policies", "Locations", "Marketing events", "Merchant-managed fulfillment orders", "Metaobject definitions", "Metaobject entries", "Online Store navigation", "Online Store pages", "Order editing", "Orders", "Packing slip management", "Payment customizations", "Payment terms", "Pixels", "Price rules", "Product feeds", "Product listings", "Products", "Publications", "Purchase options", "Reports", "Resource feedback", "Returns", "Sales channels", "Script tags", "Shipping", "Shop locales", "Shopify Markets", "Shopify Payments accounts", "Shopify Payments bank accounts", "Shopify Payments disputes", "Shopify Payments payouts", "Store content", "Store credit account transactions", "Store credit accounts", "Themes", "Third-party fulfillment orders", "Translations", "all_cart_transforms", "all_checkout_completion_target_customizations", "cart_transforms", "cash_tracking", "companies", "custom_fulfillment_services", "customer_data_erasure", "customer_merge", "delivery_customizations", "delivery_option_generators", "discounts_allocator_functions", "fulfillment_constraint_rules", "gates", "order_submission_rules", "privacy_settings", "shopify_payments_provider_accounts_sensitive", "validations"}
for _, category := range categoryOrder {
if val, ok := accessScopes[category]; ok {
t.AppendRow([]interface{}{color.GreenString(category), color.GreenString(val.Description), color.GreenString(val.PrintScopes())})
}
}
t.Render()
}

View file

@ -0,0 +1,90 @@
package slack
// SCOPES := []string{string} {
// "admin.analytics:read" : {
// "admin.analytics.getFile",
// "admin.analytics.getUsage",
// "admin.analytics.listFiles",
// }
// }
var scope_mapping = map[string][]string{
"admin.analytics:read": {"admin.analytics.getFile"},
"admin.app_activities:read": {"admin.apps.activities.list"},
"admin.apps:write": {"admin.apps.approve", "admin.apps.clearResolution", "admin.apps.config.set", "admin.apps.requests.cancel", "admin.apps.restrict", "admin.apps.uninstall"},
"admin.apps:read": {"admin.apps.approved.list", "admin.apps.config.lookup", "admin.apps.requests.list", "admin.apps.restricted.list"},
"admin.users:write": {"admin.auth.policy.assignEntities", "admin.auth.policy.removeEntities", "admin.users.assign", "admin.users.invite", "admin.users.remove", "admin.users.session.clearSettings", "admin.users.session.invalidate", "admin.users.session.reset", "admin.users.session.resetBulk", "admin.users.session.setSettings", "admin.users.setAdmin", "admin.users.setExpiration", "admin.users.setOwner", "admin.users.setRegular"},
"admin.users:read": {"admin.auth.policy.getEntities", "admin.users.list", "admin.users.session.getSettings", "admin.users.session.list", "admin.users.unsupportedVersions.export"},
"admin.barriers:write": {"admin.barriers.create", "admin.barriers.delete", "admin.barriers.update"},
"admin.barriers:read": {"admin.barriers.list"},
"admin.conversations:write": {"admin.conversations.archive", "admin.conversations.bulkArchive", "admin.conversations.bulkDelete", "admin.conversations.bulkMove", "admin.conversations.convertToPrivate", "admin.conversations.convertToPublic", "admin.conversations.create", "admin.conversations.delete", "admin.conversations.disconnectShared", "admin.conversations.invite", "admin.conversations.removeCustomRetention", "admin.conversations.rename", "admin.conversations.restrictAccess.addGroup", "admin.conversations.restrictAccess.removeGroup", "admin.conversations.setConversationPrefs", "admin.conversations.setCustomRetention", "admin.conversations.setTeams", "admin.conversations.unarchive"},
"admin.conversations:read": {"admin.conversations.ekm.listOriginalConnectedChannelInfo", "admin.conversations.getConversationPrefs", "admin.conversations.getCustomRetention", "admin.conversations.getTeams", "admin.conversations.lookup", "admin.conversations.restrictAccess.listGroups", "admin.conversations.search"},
"admin.teams:write": {"admin.emoji.add", "admin.emoji.addAlias", "admin.emoji.remove", "admin.emoji.rename", "admin.teams.create", "admin.teams.settings.setDefaultChannels", "admin.teams.settings.setDescription", "admin.teams.settings.setDiscoverability", "admin.teams.settings.setIcon", "admin.teams.settings.setName", "admin.usergroups.addTeams"},
"admin.teams:read": {"admin.emoji.list", "admin.teams.admins.list", "admin.teams.list", "admin.teams.owners.list", "admin.teams.settings.info"},
"admin.workflows:read": {"admin.functions.list", "admin.functions.permissions.lookup", "admin.workflows.permissions.lookup", "admin.workflows.search"},
"admin.workflows:write": {"admin.functions.permissions.set", "admin.workflows.collaborators.add", "admin.workflows.collaborators.remove", "admin.workflows.unpublish"},
"admin.invites:write": {"admin.inviteRequests.approve", "admin.inviteRequests.deny"},
"admin.invites:read": {"admin.inviteRequests.approved.list", "admin.inviteRequests.denied.list", "admin.inviteRequests.list"},
"admin.roles:write": {"admin.roles.addAssignments", "admin.roles.removeAssignments"},
"admin.roles:read": {"admin.roles.listAssignments"},
"admin.usergroups:write": {"admin.usergroups.addChannels", "admin.usergroups.removeChannels"},
"admin.usergroups:read": {"admin.usergroups.listChannels"},
"hosting:read": {"apps.activities.list"},
"connections:write": {"apps.connections.open"},
"token": {"apps.datastore.bulkDelete", "apps.datastore.bulkGet", "apps.datastore.bulkPut", "apps.datastore.delete", "apps.datastore.get", "apps.datastore.put", "apps.datastore.query", "apps.datastore.update"},
"datastore:read": {"apps.datastore.count"},
"authorizations:read": {"apps.event.authorizations.list"},
"bot": {"auth.revoke", "auth.test", "chat.getPermalink", "chat.scheduledMessages.list", "dialog.open", "functions.completeError", "functions.completeSuccess", "rtm.connect", "rtm.start", "views.open", "views.publish", "views.push", "views.update"},
"bookmarks:write": {"bookmarks.add", "bookmarks.edit", "bookmarks.remove"},
"bookmarks:read": {"bookmarks.list"},
"users:read": {"bots.info", "users.getPresence", "users.info", "users.list"},
"calls:write": {"calls.add", "calls.end", "calls.participants.add", "calls.participants.remove", "calls.update"},
"calls:read": {"calls.info"},
"channels:manage": {"channels.create", "channels.mark", "conversations.archive", "conversations.close", "conversations.create", "conversations.kick", "conversations.leave", "conversations.mark", "conversations.open", "conversations.rename", "conversations.unarchive", "groups.create", "groups.mark", "im.mark", "im.open", "mpim.mark", "mpim.open"},
"channels:read": {"channels.info", "conversations.info", "conversations.list", "conversations.members", "groups.info", "im.list", "mpim.list", "users.conversations"},
"channels:write.invites": {"channels.invite", "conversations.invite", "groups.invite"},
"chat:write": {"chat.delete", "chat.deleteScheduledMessage", "chat.meMessage", "chat.postEphemeral", "chat.postMessage", "chat.scheduleMessage", "chat.update"},
"links:write": {"chat.unfurl"},
"conversations.connect:write": {"conversations.acceptSharedInvite", "conversations.inviteShared"},
"conversations.connect:manage": {"conversations.approveSharedInvite", "conversations.declineSharedInvite", "conversations.listConnectInvites"},
"channels:history": {"conversations.history", "conversations.replies"},
"channels:join": {"conversations.join"},
"channels:write.topic": {"conversations.setPurpose", "conversations.setTopic"},
"dnd:write": {"dnd.endDnd", "dnd.endSnooze", "dnd.setSnooze"},
"dnd:read": {"dnd.info", "dnd.teamInfo"},
"emoji:read": {"emoji.list"},
"files:write": {"files.comments.delete", "files.completeUploadExternal", "files.delete", "files.getUploadURLExternal", "files.revokePublicURL", "files.sharedPublicURL", "files.upload"},
"files:read": {"files.info", "files.list"},
"remote_files:write": {"files.remote.add", "files.remote.remove", "files.remote.update"},
"remote_files:read": {"files.remote.info", "files.remote.list"},
"remote_files:share": {"files.remote.share"},
"app_configurations:write": {"functions.distributions.permissions.add", "functions.distributions.permissions.remove", "functions.distributions.permissions.set"},
"app_configurations:read": {"functions.distributions.permissions.list"},
"conversations": {"groups.open"},
"tokens.basic": {"migration.exchange"},
"email": {"openid.connect.userInfo"},
"pins:write": {"pins.add", "pins.remove"},
"pins:read": {"pins.list"},
"reactions:write": {"reactions.add", "reactions.remove"},
"reactions:read": {"reactions.get", "reactions.list"},
"reminders:write": {"reminders.add", "reminders.complete", "reminders.delete"},
"reminders:read": {"reminders.info", "reminders.list"},
"search:read": {"search.all", "search.files", "search.messages"},
"stars:write": {"stars.add", "stars.remove"},
"stars:read": {"stars.list"},
"admin": {"team.accessLogs", "team.billableInfo", "team.integrationLogs"},
"team.billing:read": {"team.billing.info"},
"team:read": {"team.info"},
"team.preferences:read": {"team.preferences.list"},
"users.profile:read": {"team.profile.get", "users.profile.get"},
"usergroups:write": {"usergroups.create", "usergroups.disable", "usergroups.enable", "usergroups.update", "usergroups.users.update"},
"usergroups:read": {"usergroups.list", "usergroups.users.list"},
"users.profile:write": {"users.deletePhoto", "users.profile.set", "users.setPhoto"},
"identity.basic": {"users.identity"},
"users:read.email": {"users.lookupByEmail"},
"users:write": {"users.setActive", "users.setPresence"},
"workflow.steps:execute": {"workflows.stepCompleted", "workflows.stepFailed", "workflows.updateStep"},
"triggers:write": {"workflows.triggers.permissions.add", "workflows.triggers.permissions.remove", "workflows.triggers.permissions.set"},
"triggers:read": {"workflows.triggers.permissions.list"},
}

View file

@ -0,0 +1,141 @@
package slack
import (
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"strings"
"github.com/fatih/color"
"github.com/jedib0t/go-pretty/table"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/config"
)
// Add in showAll to printScopes + deal with testing enterprise + add scope details
type SlackUserData struct {
Ok bool `json:"ok"`
Url string `json:"url"`
Team string `json:"team"`
User string `json:"user"`
TeamId string `json:"team_id"`
UserId string `json:"user_id"`
BotId string `json:"bot_id"`
IsEnterprise bool `json:"is_enterprise"`
}
func getSlackOAuthScopes(cfg *config.Config, key string) (scopes string, userData SlackUserData, err error) {
userData = SlackUserData{}
scopes = ""
// URL to which the request will be sent
url := "https://slack.com/api/auth.test"
// Create a client to send the request
client := analyzers.NewAnalyzeClient(cfg)
// Create the request
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return scopes, userData, err
}
// Add the Authorization header to the request
req.Header.Add("Authorization", "Bearer "+key)
// Send the request
resp, err := client.Do(req)
if err != nil {
return scopes, userData, err
}
defer resp.Body.Close() // Close the response body when the function returns
// print body
body, err := io.ReadAll(resp.Body)
if err != nil {
return scopes, userData, err
}
// Unmarshal the response body into the SlackUserData struct
if err := json.Unmarshal(body, &userData); err != nil {
return scopes, userData, err
}
// Print all headers received from the server
scopes = resp.Header.Get("X-Oauth-Scopes")
return scopes, userData, err
}
func AnalyzePermissions(cfg *config.Config, key string) {
scopes, userData, err := getSlackOAuthScopes(cfg, key)
if err != nil {
color.Red("[!] Error getting Slack OAuth scopes:", err)
return
}
if !userData.Ok {
color.Red("[!] Invalid Slack Token")
return
}
color.Green("[!] Valid Slack API Key\n\n")
printIdentityInfo(userData)
printScopes(strings.Split(scopes, ","))
}
func printIdentityInfo(userData SlackUserData) {
if userData.Url != "" {
color.Green("URL: %v", userData.Url)
}
if userData.Team != "" {
color.Green("Team: %v", userData.Team)
}
if userData.User != "" {
color.Green("User: %v", userData.User)
}
if userData.TeamId != "" {
color.Green("Team ID: %v", userData.TeamId)
}
if userData.UserId != "" {
color.Green("User ID: %v", userData.UserId)
}
if userData.BotId != "" {
color.Green("Bot ID: %v", userData.BotId)
}
fmt.Println("")
if userData.IsEnterprise {
color.Green("[!] Slack is Enterprise")
} else {
color.Yellow("[-] Slack is not Enterprise")
}
fmt.Println("")
}
func printScopes(scopes []string) {
t := table.NewWriter()
// if !showAll {
// t.SetOutputMirror(os.Stdout)
// t.AppendHeader(table.Row{"Scopes"})
// for _, scope := range scopes {
// t.AppendRow([]interface{}{color.GreenString(scope)})
// }
// } else {
t.SetOutputMirror(os.Stdout)
t.AppendHeader(table.Row{"Scope", "Permissions"})
for _, scope := range scopes {
perms := scope_mapping[scope]
if perms == nil {
t.AppendRow([]interface{}{color.GreenString(scope), color.GreenString("")})
} else {
t.AppendRow([]interface{}{color.GreenString(scope), color.GreenString(strings.Join(perms, ", "))})
}
}
//}
t.Render()
}

View file

@ -0,0 +1,139 @@
package sourcegraph
// ToDo: Add suport for custom domain
import (
"encoding/json"
"net/http"
"strings"
"github.com/fatih/color"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/config"
)
type GraphQLError struct {
Message string `json:"message"`
Path []string `json:"path"`
}
type GraphQLResponse struct {
Errors []GraphQLError `json:"errors"`
Data interface{} `json:"data"`
}
type UserInfoJSON struct {
Data struct {
CurrentUser struct {
Username string `json:"username"`
Email string `json:"email"`
SiteAdmin bool `json:"siteAdmin"`
CreatedAt string `json:"createdAt"`
} `json:"currentUser"`
} `json:"data"`
}
func getUserInfo(cfg *config.Config, key string) (UserInfoJSON, error) {
var userInfo UserInfoJSON
client := analyzers.NewAnalyzeClient(cfg)
payload := "{\"query\":\"query { currentUser { username, email, siteAdmin, createdAt } }\"}"
req, err := http.NewRequest("POST", "https://sourcegraph.com/.api/graphql", strings.NewReader(payload))
if err != nil {
return userInfo, err
}
req.Header.Set("Authorization", "token "+key)
resp, err := client.Do(req)
if err != nil {
return userInfo, err
}
defer resp.Body.Close()
err = json.NewDecoder(resp.Body).Decode(&userInfo)
if err != nil {
return userInfo, err
}
return userInfo, nil
}
func checkSiteAdmin(cfg *config.Config, key string) (bool, error) {
query := `
{
"query": "query webhooks($first: Int, $after: String, $kind: ExternalServiceKind) { webhooks(first: $first, after: $after, kind: $kind) { totalCount } }",
"variables": {
"first": 10,
"after": "",
"kind": "GITHUB"
}
}`
client := analyzers.NewAnalyzeClient(cfg)
req, err := http.NewRequest("POST", "https://sourcegraph.com/.api/graphql", strings.NewReader(query))
if err != nil {
return false, err
}
req.Header.Set("Authorization", "token "+key)
resp, err := client.Do(req)
if err != nil {
return false, err
}
defer resp.Body.Close()
var response GraphQLResponse
err = json.NewDecoder(resp.Body).Decode(&response)
if err != nil {
return false, err
}
if len(response.Errors) > 0 {
return false, nil
}
return true, nil
}
func AnalyzePermissions(cfg *config.Config, key string) {
userInfo, err := getUserInfo(cfg, key)
if err != nil {
color.Red("Error: %s", err)
return
}
// second call
userInfo, err = getUserInfo(cfg, key)
if err != nil {
color.Red("Error: %s", err)
return
}
if userInfo.Data.CurrentUser.Username == "" {
color.Red("[x] Invalid Sourcegraph Access Token")
return
}
color.Green("[!] Valid Sourcegraph Access Token\n\n")
color.Yellow("[i] Sourcegraph User Information\n")
color.Green("Username: %s\n", userInfo.Data.CurrentUser.Username)
color.Green("Email: %s\n", userInfo.Data.CurrentUser.Email)
color.Green("Created At: %s\n\n", userInfo.Data.CurrentUser.CreatedAt)
isSiteAdmin, err := checkSiteAdmin(cfg, key)
if err != nil {
color.Red("Error: %s", err)
return
}
if isSiteAdmin {
color.Green("[!] Token Permissions: Site Admin")
} else {
// This is the default for all access tokens as of 6/11/24
color.Yellow("[i] Token Permissions: user:full (default)")
}
}

View file

@ -0,0 +1,408 @@
package square
var permissions_slice = []map[string]map[string][]string{
{
"Bank Accounts": {
"GetBankAccount": []string{"BANK_ACCOUNTS_READ"},
"ListBankAccounts": []string{"BANK_ACCOUNTS_READ"},
"GetBankAccountByV1Id": []string{"BANK_ACCOUNTS_READ"},
},
},
{
"Bookings": {
"CreateBooking (buyer-level)": []string{"APPOINTMENTS_WRITE"},
"CreateBooking (seller-level)": []string{"APPOINTMENTS_WRITE", "APPOINTMENTS_ALL_WRITE"},
"SearchAvailability (buyer-level)": []string{"APPOINTMENTS_READ"},
"SearchAvailability (seller-level)": []string{"APPOINTMENTS_READ", "APPOINTMENTS_ALL_READ"},
"RetrieveBusinessBookingProfile": []string{"APPOINTMENTS_BUSINESS_SETTINGS_READ"},
"ListTeamMemberBookingProfiles": []string{"APPOINTMENTS_BUSINESS_SETTINGS_READ"},
"RetrieveTeamMemberBookingProfile": []string{"APPOINTMENTS_BUSINESS_SETTINGS_READ"},
"ListBookings (buyer-level)": []string{"APPOINTMENTS_READ"},
"ListBookings (seller-level)": []string{"APPOINTMENTS_READ", "APPOINTMENTS_ALL_READ"},
"RetrieveBooking (buyer-level)": []string{"APPOINTMENTS_READ"},
"RetrieveBooking (seller-level)": []string{"APPOINTMENTS_READ", "APPOINTMENTS_ALL_READ"},
"UpdateBooking (buyer-level)": []string{"APPOINTMENTS_WRITE"},
"UpdateBooking (seller-level)": []string{"APPOINTMENTS_WRITE", "APPOINTMENTS_ALL_WRITE"},
"CancelBooking (buyer-level)": []string{"APPOINTMENTS_WRITE"},
"CancelBooking (seller-level)": []string{"APPOINTMENTS_WRITE", "APPOINTMENTS_ALL_WRITE"},
},
},
{
"Booking Custom Attributes": {
"CreateBookingCustomAttributeDefinition (buyer-level)": []string{"APPOINTMENTS_WRITE"},
"CreateBookingCustomAttributeDefinition (seller-level)": []string{"APPOINTMENTS_WRITE", "APPOINTMENTS_ALL_WRITE"},
"UpdateBookingCustomAttributeDefinition (buyer-level)": []string{"APPOINTMENTS_WRITE"},
"UpdateBookingCustomAttributeDefinition (seller-level)": []string{"APPOINTMENTS_WRITE", "APPOINTMENTS_ALL_WRITE"},
"ListBookingCustomAttributeDefinitions (buyer-level)": []string{"APPOINTMENTS_READ"},
"ListBookingCustomAttributeDefinitions (seller-level)": []string{"APPOINTMENTS_READ", "APPOINTMENTS_ALL_READ"},
"RetrieveBookingCustomAttributeDefinition (buyer-level)": []string{"APPOINTMENTS_READ"},
"RetrieveBookingCustomAttributeDefinition (seller-level)": []string{"APPOINTMENTS_READ", "APPOINTMENTS_ALL_READ"},
"DeleteBookingCustomAttributeDefinition (buyer-level)": []string{"APPOINTMENTS_WRITE"},
"DeleteBookingCustomAttributeDefinition (seller-level)": []string{"APPOINTMENTS_WRITE", "APPOINTMENTS_ALL_WRITE"},
"UpsertBookingCustomAttribute (buyer-level)": []string{"APPOINTMENTS_WRITE"},
"UpsertBookingCustomAttribute (seller-level)": []string{"APPOINTMENTS_WRITE", "APPOINTMENTS_ALL_WRITE"},
"BulkUpsertBookingCustomAttributes (buyer-level)": []string{"APPOINTMENTS_WRITE"},
"BulkUpsertBookingCustomAttributes (seller-level)": []string{"APPOINTMENTS_WRITE", "APPOINTMENTS_ALL_WRITE"},
"ListBookingCustomAttributes (buyer-level)": []string{"APPOINTMENTS_READ"},
"ListBookingCustomAttributes (seller-level)": []string{"APPOINTMENTS_READ", "APPOINTMENTS_ALL_READ"},
"RetrieveBookingCustomAttribute (buyer-level)": []string{"APPOINTMENTS_READ"},
"RetrieveBookingCustomAttribute (seller-level)": []string{"APPOINTMENTS_READ", "APPOINTMENTS_ALL_READ"},
"DeleteBookingCustomAttribute (buyer-level)": []string{"APPOINTMENTS_WRITE"},
"DeleteBookingCustomAttribute (seller-level)": []string{"APPOINTMENTS_WRITE", "APPOINTMENTS_ALL_WRITE"},
},
},
{
"Cards": {
"ListCards": []string{"PAYMENTS_READ"},
"CreateCard": []string{"PAYMENTS_WRITE"},
"RetrieveCard": []string{"PAYMENTS_READ"},
"DisableCard": []string{"PAYMENTS_WRITE"},
},
},
{
"Cash Drawer Shifts": {
"ListCashDrawerShifts": []string{"CASH_DRAWER_READ"},
"ListCashDrawerShiftEvents": []string{"CASH_DRAWER_READ"},
"RetrieveCashDrawerShift": []string{"CASH_DRAWER_READ"},
},
},
{
"Catalog": {
"BatchDeleteCatalogObjects": []string{"ITEMS_WRITE"},
"BatchUpsertCatalogObjects": []string{"ITEMS_WRITE"},
"BatchRetrieveCatalogObjects": []string{"ITEMS_READ"},
"CatalogInfo": []string{"ITEMS_READ"},
"CreateCatalogImage": []string{"ITEMS_WRITE"},
"DeleteCatalogObject": []string{"ITEMS_WRITE"},
"ListCatalog": []string{"ITEMS_READ"},
"RetrieveCatalogObject": []string{"ITEMS_READ"},
"SearchCatalogItems": []string{"ITEMS_READ"},
"SearchCatalogObjects": []string{"ITEMS_READ"},
"UpdateItemTaxes": []string{"ITEMS_WRITE"},
"UpdateItemModifierLists": []string{"ITEMS_WRITE"},
"UpsertCatalogObject": []string{"ITEMS_WRITE"},
},
},
{
"Checkout": {
"CreatePaymentLink": []string{"ORDERS_WRITE", "ORDERS_READ", "PAYMENTS_WRITE"},
},
},
{
"Customers": {
"AddGroupToCustomer": []string{"CUSTOMERS_WRITE"},
"BulkCreateCustomers": []string{"CUSTOMERS_WRITE"},
"BulkDeleteCustomers": []string{"CUSTOMERS_WRITE"},
"BulkRetrieveCustomers": []string{"CUSTOMERS_READ"},
"BulkUpdateCustomers": []string{"CUSTOMERS_WRITE"},
"CreateCustomer": []string{"CUSTOMERS_WRITE"},
"CreateCustomerCard (deprecated)": []string{"CUSTOMERS_WRITE"},
"DeleteCustomer": []string{"CUSTOMERS_WRITE"},
"DeleteCustomerCard (deprecated)": []string{"CUSTOMERS_WRITE"},
"ListCustomers": []string{"CUSTOMERS_READ"},
"RemoveGroupFromCustomer": []string{"CUSTOMERS_WRITE"},
"RetrieveCustomer": []string{"CUSTOMERS_READ"},
"SearchCustomers": []string{"CUSTOMERS_READ"},
"UpdateCustomer": []string{"CUSTOMERS_WRITE"},
},
},
{
"Customer Custom Attributes": {
"CreateCustomerCustomAttributeDefinition": []string{"CUSTOMERS_WRITE"},
"UpdateCustomerCustomAttributeDefinition": []string{"CUSTOMERS_WRITE"},
"ListCustomerCustomAttributeDefinitions": []string{"CUSTOMERS_READ"},
"RetrieveCustomerCustomAttributeDefinition": []string{"CUSTOMERS_READ"},
"DeleteCustomerCustomAttributeDefinition": []string{"CUSTOMERS_WRITE"},
"UpsertCustomerCustomAttribute": []string{"CUSTOMERS_WRITE"},
"BulkUpsertCustomerCustomAttributes": []string{"CUSTOMERS_WRITE"},
"ListCustomerCustomAttributes": []string{"CUSTOMERS_READ"},
"RetrieveCustomerCustomAttribute": []string{"CUSTOMERS_READ"},
"DeleteCustomerCustomAttribute": []string{"CUSTOMERS_WRITE"},
},
},
{
"Customer Groups": {
"CreateCustomerGroup": []string{"CUSTOMERS_WRITE"},
"DeleteCustomerGroup": []string{"CUSTOMERS_WRITE"},
"ListCustomerGroups": []string{"CUSTOMERS_READ"},
"RetrieveCustomerGroup": []string{"CUSTOMERS_READ"},
"UpdateCustomerGroup": []string{"CUSTOMERS_WRITE"},
},
},
{
"Customer Segments": {
"ListCustomerSegments": []string{"CUSTOMERS_READ"},
"RetrieveCustomerSegment": []string{"CUSTOMERS_READ"},
},
},
{
"Devices": {
"CreateDeviceCode": []string{"DEVICE_CREDENTIAL_MANAGEMENT"},
"GetDeviceCode": []string{"DEVICE_CREDENTIAL_MANAGEMENT"},
"ListDeviceCodes": []string{"DEVICE_CREDENTIAL_MANAGEMENT"},
"ListDevices": []string{"DEVICES_READ"},
"GetDevice": []string{"DEVICES_READ"},
},
},
{
"Disputes": {
"AcceptDispute": []string{"DISPUTES_WRITE"},
"CreateDisputeEvidenceFile": []string{"DISPUTES_WRITE"},
"CreateDisputeEvidenceText": []string{"DISPUTES_WRITE"},
"ListDisputeEvidence": []string{"DISPUTES_READ"},
"ListDisputes": []string{"DISPUTES_READ"},
"DeleteDisputeEvidence": []string{"DISPUTES_WRITE"},
"RetrieveDispute": []string{"DISPUTES_READ"},
"RetrieveDisputeEvidence": []string{"DISPUTES_READ"},
"SubmitEvidence": []string{"DISPUTES_WRITE"},
},
},
{
"Employees": {
"ListEmployees (deprecated)": []string{"EMPLOYEES_READ"},
"RetrieveEmployee (deprecated)": []string{"EMPLOYEES_READ"},
},
},
{
"Gift Cards": {
"ListGiftCards": []string{"GIFTCARDS_READ"},
"CreateGiftCard": []string{"GIFTCARDS_WRITE"},
"RetrieveGiftCard": []string{"GIFTCARDS_READ"},
"RetrieveGiftCardFromGAN": []string{"GIFTCARDS_READ"},
"RetrieveGiftCardFromNonce": []string{"GIFTCARDS_READ"},
"LinkCustomerToGiftCard": []string{"GIFTCARDS_WRITE"},
"UnlinkCustomerFromGiftCard": []string{"GIFTCARDS_WRITE"},
},
},
{
"Gift Card Activities": {
"ListGiftCardActivities": []string{"GIFTCARDS_READ"},
"CreateGiftCardActivity": []string{"GIFTCARDS_WRITE"},
},
},
{
"Inventory": {
"BatchChangeInventory": []string{"INVENTORY_WRITE"},
"BatchRetrieveInventoryCounts": []string{"INVENTORY_READ"},
"BatchRetrieveInventoryChanges": []string{"INVENTORY_READ"},
"RetrieveInventoryAdjustment": []string{"INVENTORY_READ"},
"RetrieveInventoryChanges": []string{"INVENTORY_READ"},
"RetrieveInventoryCount": []string{"INVENTORY_READ"},
"RetrieveInventoryPhysicalCount": []string{"INVENTORY_READ"},
},
},
{
"Invoices": {
"CreateInvoice": []string{"INVOICES_WRITE", "ORDERS_WRITE"},
"PublishInvoice": []string{"CUSTOMERS_READ", "PAYMENTS_WRITE", "INVOICES_WRITE", "ORDERS_WRITE"},
"GetInvoice": []string{"INVOICES_READ"},
"ListInvoices": []string{"INVOICES_READ"},
"SearchInvoices": []string{"INVOICES_READ"},
"CreateInvoiceAttachment": []string{"INVOICES_WRITE", "ORDERS_WRITE"},
"DeleteInvoiceAttachment": []string{"INVOICES_WRITE", "ORDERS_WRITE"},
"UpdateInvoice": []string{"INVOICES_WRITE", "ORDERS_WRITE"},
"DeleteInvoice": []string{"INVOICES_WRITE", "ORDERS_WRITE"},
"CancelInvoice": []string{"INVOICES_WRITE", "ORDERS_WRITE"},
},
},
{
"Labor": {
"CreateBreakType": []string{"TIMECARDS_SETTINGS_WRITE"},
"CreateShift": []string{"TIMECARDS_WRITE"},
"DeleteBreakType": []string{"TIMECARDS_SETTINGS_WRITE"},
"DeleteShift": []string{"TIMECARDS_WRITE"},
"GetBreakType": []string{"TIMECARDS_SETTINGS_READ"},
"GetTeamMemberWage": []string{"EMPLOYEES_READ"},
"GetShift": []string{"TIMECARDS_READ"},
"ListBreakTypes": []string{"TIMECARDS_SETTINGS_READ"},
"ListTeamMemberWages": []string{"EMPLOYEES_READ"},
"ListWorkweekConfigs": []string{"TIMECARDS_SETTINGS_READ"},
"SearchShifts": []string{"TIMECARDS_READ"},
"UpdateShift": []string{"TIMECARDS_WRITE", "TIMECARDS_READ"},
"UpdateWorkweekConfig": []string{"TIMECARDS_SETTINGS_WRITE", "TIMECARDS_SETTINGS_READ"},
"UpdateBreakType": []string{"TIMECARDS_SETTINGS_WRITE", "TIMECARDS_SETTINGS_READ"},
},
},
{
"Locations": {
"CreateLocation": []string{"MERCHANT_PROFILE_WRITE"},
"ListLocations": []string{"MERCHANT_PROFILE_READ"},
"RetrieveLocation": []string{"MERCHANT_PROFILE_READ"},
"UpdateLocation": []string{"MERCHANT_PROFILE_WRITE"},
},
},
{
"Location Custom Attributes": {
"CreateLocationCustomAttributeDefinition": []string{"MERCHANT_PROFILE_WRITE"},
"UpdateLocationCustomAttributeDefinition": []string{"MERCHANT_PROFILE_WRITE"},
"ListLocationCustomAttributeDefinitions": []string{"MERCHANT_PROFILE_READ"},
"RetrieveLocationCustomAttributeDefinition": []string{"MERCHANT_PROFILE_READ"},
"DeleteLocationCustomAttributeDefinition": []string{"MERCHANT_PROFILE_WRITE"},
"UpsertLocationCustomAttribute": []string{"MERCHANT_PROFILE_WRITE"},
"BulkUpsertLocationCustomAttributes": []string{"MERCHANT_PROFILE_WRITE"},
"ListLocationCustomAttributes": []string{"MERCHANT_PROFILE_READ"},
"RetrieveLocationCustomAttribute": []string{"MERCHANT_PROFILE_READ"},
"DeleteLocationCustomAttribute": []string{"MERCHANT_PROFILE_WRITE"},
"BulkDeleteLocationCustomAttributes": []string{"MERCHANT_PROFILE_WRITE"},
},
},
{
"Loyalty": {
"RetrieveLoyaltyProgram": []string{"LOYALTY_READ"},
"ListLoyaltyPrograms (deprecated)": []string{"LOYALTY_READ"},
"CreateLoyaltyPromotion": []string{"LOYALTY_WRITE"},
"ListLoyaltyPromotions": []string{"LOYALTY_READ"},
"RetrieveLoyaltyPromotion": []string{"LOYALTY_READ"},
"CancelLoyaltyPromotion": []string{"LOYALTY_WRITE"},
"CreateLoyaltyAccount": []string{"LOYALTY_WRITE"},
"RetrieveLoyaltyAccount": []string{"LOYALTY_READ"},
"SearchLoyaltyAccounts": []string{"LOYALTY_READ"},
"AccumulateLoyaltyPoints": []string{"LOYALTY_WRITE"},
"AdjustLoyaltyPoints": []string{"LOYALTY_WRITE"},
"CalculateLoyaltyPoints": []string{"LOYALTY_READ"},
"CreateLoyaltyReward": []string{"LOYALTY_WRITE"},
"RedeemLoyaltyReward": []string{"LOYALTY_WRITE"},
"RetrieveLoyaltyReward": []string{"LOYALTY_READ"},
"SearchLoyaltyRewards": []string{"LOYALTY_READ"},
"DeleteLoyaltyReward": []string{"LOYALTY_WRITE"},
"SearchLoyaltyEvents": []string{"LOYALTY_READ"},
},
},
{
"Merchants": {
"ListMerchants": []string{"MERCHANT_PROFILE_READ"},
"RetrieveMerchant": []string{"MERCHANT_PROFILE_READ"},
},
},
{
"Merchant Custom Attributes": {
"CreateMerchantCustomAttributeDefinition": []string{"MERCHANT_PROFILE_WRITE"},
"UpdateMerchantCustomAttributeDefinition": []string{"MERCHANT_PROFILE_WRITE"},
"ListMerchantCustomAttributeDefinitions": []string{"MERCHANT_PROFILE_READ"},
"RetrieveMerchantCustomAttributeDefinition": []string{"MERCHANT_PROFILE_READ"},
"DeleteMerchantCustomAttributeDefinition": []string{"MERCHANT_PROFILE_WRITE"},
"UpsertMerchantCustomAttribute": []string{"MERCHANT_PROFILE_WRITE"},
"BulkUpsertMerchantCustomAttributes": []string{"MERCHANT_PROFILE_WRITE"},
"ListMerchantCustomAttributes": []string{"MERCHANT_PROFILE_READ"},
"RetrieveMerchantCustomAttribute": []string{"MERCHANT_PROFILE_READ"},
"DeleteMerchantCustomAttribute": []string{"MERCHANT_PROFILE_WRITE"},
"BulkDeleteMerchantCustomAttributes": []string{"MERCHANT_PROFILE_WRITE"},
},
},
{
"Mobile Authorization": {
"CreateMobileAuthorizationCode": []string{"PAYMENTS_WRITE_IN_PERSON"},
},
},
{
"Orders": {
"CloneOrder": []string{"ORDERS_WRITE"},
"CreateOrder": []string{"ORDERS_WRITE"},
"BatchRetrieveOrders": []string{"ORDERS_READ"},
"PayOrder": []string{"ORDERS_WRITE", "PAYMENTS_WRITE"},
"RetrieveOrder": []string{"ORDERS_WRITE", "ORDERS_READ"},
"SearchOrders": []string{"ORDERS_READ"},
"UpdateOrder": []string{"ORDERS_WRITE"},
},
},
{
"Order Custom Attributes": {
"CreateOrderCustomAttributeDefinition": []string{"ORDERS_WRITE"},
"UpdateOrderCustomAttributeDefinition": []string{"ORDERS_WRITE"},
"ListOrderCustomAttributeDefinitions": []string{"ORDERS_READ"},
"RetrieveOrderCustomAttributeDefinition": []string{"ORDERS_READ"},
"DeleteOrderCustomAttributeDefinition": []string{"ORDERS_WRITE"},
"UpsertOrderCustomAttribute": []string{"ORDERS_WRITE"},
"BulkUpsertOrderCustomAttributes": []string{"ORDERS_WRITE"},
"ListOrderCustomAttributes": []string{"ORDERS_READ"},
"RetrieveOrderCustomAttribute": []string{"ORDERS_READ"},
"DeleteOrderCustomAttribute": []string{"ORDERS_WRITE"},
"BulkDeleteOrderCustomAttributes": []string{"ORDERS_WRITE"},
},
},
{
"Payments and Refunds": {
"CancelPayment": []string{"PAYMENTS_WRITE"},
"CancelPaymentByIdempotencyKey": []string{"PAYMENTS_WRITE"},
"CompletePayment": []string{"PAYMENTS_WRITE"},
"CreatePayment": []string{"PAYMENTS_WRITE", "PAYMENTS_WRITE_SHARED_ONFILE", "PAYMENTS_WRITE_ADDITIONAL_RECIPIENTS"},
"GetPayment": []string{"PAYMENTS_READ"},
"GetPaymentRefund": []string{"PAYMENTS_READ"},
"ListPayments": []string{"PAYMENTS_READ"},
"ListPaymentRefunds": []string{"PAYMENTS_READ"},
"RefundPayment": []string{"PAYMENTS_WRITE", "PAYMENTS_WRITE_ADDITIONAL_RECIPIENTS"},
},
},
{
"Payouts": {
"ListPayouts": []string{"PAYOUTS_READ"},
"GetPayout": []string{"PAYOUTS_READ"},
"ListPayoutEntries": []string{"PAYOUTS_READ"},
},
},
{
"Sites": {
"ListSites": []string{"ONLINE_STORE_SITE_READ"},
},
},
{
"Snippets": {
"UpsertSnippet": []string{"ONLINE_STORE_SNIPPETS_WRITE"},
"RetrieveSnippet": []string{"ONLINE_STORE_SNIPPETS_READ"},
"DeleteSnippet": []string{"ONLINE_STORE_SNIPPETS_WRITE"},
},
},
{
"Subscriptions": {
"CreateSubscription": []string{"CUSTOMERS_READ", "PAYMENTS_WRITE", "SUBSCRIPTIONS_WRITE", "ITEMS_READ", "ORDERS_WRITE", "INVOICES_WRITE"},
"SearchSubscriptions": []string{"SUBSCRIPTIONS_READ"},
"RetrieveSubscription": []string{"SUBSCRIPTIONS_READ"},
"UpdateSubscription": []string{"CUSTOMERS_READ", "PAYMENTS_WRITE", "SUBSCRIPTIONS_WRITE", "ITEMS_READ", "ORDERS_WRITE", "INVOICES_WRITE"},
"CancelSubscription": []string{"SUBSCRIPTIONS_WRITE"},
"ListSubscriptionEvents": []string{"SUBSCRIPTIONS_READ"},
"ResumeSubscription": []string{"CUSTOMERS_READ", "PAYMENTS_WRITE", "SUBSCRIPTIONS_WRITE", "ITEMS_READ", "ORDERS_WRITE", "INVOICES_WRITE"},
"PauseSubscription": []string{"CUSTOMERS_READ", "PAYMENTS_WRITE", "SUBSCRIPTIONS_WRITE", "ITEMS_READ", "ORDERS_WRITE", "INVOICES_WRITE"},
"SwapPlan": []string{"CUSTOMERS_READ", "PAYMENTS_WRITE", "SUBSCRIPTIONS_WRITE", "ITEMS_READ", "ORDERS_WRITE", "INVOICES_WRITE"},
"DeleteSubscriptionAction": []string{"SUBSCRIPTIONS_WRITE"},
},
},
{
"Team": {
"BulkCreateTeamMembers": []string{"EMPLOYEES_WRITE"},
"BulkUpdateTeamMembers": []string{"EMPLOYEES_WRITE"},
"CreateTeamMember": []string{"EMPLOYEES_WRITE"},
"UpdateTeamMember": []string{"EMPLOYEES_WRITE"},
"RetrieveTeamMember": []string{"EMPLOYEES_READ"},
"RetrieveWageSetting": []string{"EMPLOYEES_READ"},
"SearchTeamMembers": []string{"EMPLOYEES_READ"},
"UpdateWageSetting": []string{"EMPLOYEES_WRITE"},
},
},
{
"Terminal": {
"CreateTerminalCheckout": []string{"PAYMENTS_WRITE"},
"CancelTerminalCheckout": []string{"PAYMENTS_WRITE"},
"GetTerminalCheckout": []string{"PAYMENTS_READ"},
"SearchTerminalCheckouts": []string{"PAYMENTS_READ"},
"CreateTerminalRefund": []string{"PAYMENTS_WRITE"},
"CancelTerminalRefund": []string{"PAYMENTS_WRITE"},
"GetTerminalRefund": []string{"PAYMENTS_READ"},
"SearchTerminalRefunds": []string{"PAYMENTS_READ"},
"CreateTerminalAction": []string{"PAYMENTS_WRITE"},
"CancelTerminalAction": []string{"PAYMENTS_WRITE"},
"GetTerminalAction": []string{"PAYMENTS_READ", "CUSTOMERS_READ"},
"SearchTerminalAction": []string{"PAYMENTS_READ"},
},
},
{
"Vendors": {
"BulkCreateVendors": []string{"VENDOR_WRITE"},
"BulkRetrieveVendors": []string{"VENDOR_READ"},
"BulkUpdateVendors": []string{"VENDOR_WRITE"},
"CreateVendor": []string{"VENDOR_WRITE"},
"SearchVendors": []string{"VENDOR_READ"},
"RetrieveVendor": []string{"VENDOR_READ"},
"UpdateVendors": []string{"VENDOR_WRITE"},
},
},
}

View file

@ -0,0 +1,191 @@
package square
import (
"encoding/json"
"net/http"
"os"
"strconv"
"strings"
"github.com/fatih/color"
"github.com/jedib0t/go-pretty/table"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/config"
)
type TeamJSON struct {
TeamMembers []struct {
IsOwner bool `json:"is_owner"`
FirstName string `json:"given_name"`
LastName string `json:"family_name"`
Email string `json:"email_address"`
CreatedAt string `json:"created_at"`
} `json:"team_members"`
}
type PermissionsJSON struct {
Scopes []string `json:"scopes"`
ExpiresAt string `json:"expires_at"`
ClientID string `json:"client_id"`
MerchantID string `json:"merchant_id"`
}
func getPermissions(cfg *config.Config, key string) (PermissionsJSON, error) {
var permissions PermissionsJSON
client := analyzers.NewAnalyzeClient(cfg)
req, err := http.NewRequest("POST", "https://connect.squareup.com/oauth2/token/status", nil)
if err != nil {
return permissions, err
}
req.Header.Add("Authorization", "Bearer "+key)
req.Header.Add("Content-Type", "application/json")
req.Header.Add("Square-Version", "2024-06-04")
resp, err := client.Do(req)
if err != nil {
return permissions, err
}
if resp.StatusCode != 200 {
return permissions, nil
}
defer resp.Body.Close()
err = json.NewDecoder(resp.Body).Decode(&permissions)
if err != nil {
return permissions, err
}
return permissions, nil
}
func getUsers(cfg *config.Config, key string) (TeamJSON, error) {
var team TeamJSON
client := analyzers.NewAnalyzeClient(cfg)
req, err := http.NewRequest("POST", "https://connect.squareup.com/v2/team-members/search", nil)
if err != nil {
return team, err
}
req.Header.Add("Authorization", "Bearer "+key)
req.Header.Add("Content-Type", "application/json")
req.Header.Add("Square-Version", "2024-06-04")
q := req.URL.Query()
q.Add("limit", "200")
req.URL.RawQuery = q.Encode()
resp, err := client.Do(req)
if err != nil {
return team, err
}
if resp.StatusCode != 200 {
return team, nil
}
defer resp.Body.Close()
err = json.NewDecoder(resp.Body).Decode(&team)
if err != nil {
return team, err
}
return team, nil
}
func AnalyzePermissions(cfg *config.Config, key string) {
permissions, err := getPermissions(cfg, key)
if err != nil {
color.Red("Error: %s", err)
return
}
if permissions.MerchantID == "" {
color.Red("[x] Invalid Square API Key")
return
}
color.Green("[!] Valid Square API Key\n\n")
color.Yellow("Merchant ID: %s", permissions.MerchantID)
color.Yellow("Client ID: %s", permissions.ClientID)
if permissions.ExpiresAt == "" {
color.Green("Expires: Never\n\n")
} else {
color.Yellow("Expires: %s\n\n", permissions.ExpiresAt)
}
printPermissions(permissions.Scopes, cfg.ShowAll)
team, err := getUsers(cfg, key)
if err != nil {
color.Red("Error: %s", err)
return
}
printTeamMembers(team)
}
func contains(s []string, e string) bool {
for _, a := range s {
if a == e {
return true
}
}
return false
}
func printPermissions(scopes []string, showAll bool) {
isAccessToken := true
t := table.NewWriter()
t.SetOutputMirror(os.Stdout)
t.AppendHeader(table.Row{"API Category", "Accessible Endpoints"})
for _, permissions_slice := range permissions_slice {
for category, permissions := range permissions_slice {
accessibleEndpoints := []string{}
for endpoint, requiredPermissions := range permissions {
hasAllPermissions := true
for _, permission := range requiredPermissions {
if !contains(scopes, permission) {
hasAllPermissions = false
isAccessToken = false
break
}
}
if hasAllPermissions {
accessibleEndpoints = append(accessibleEndpoints, endpoint)
}
}
if len(accessibleEndpoints) == 0 {
t.AppendRow([]interface{}{category, ""})
} else {
t.AppendRow([]interface{}{color.GreenString(category), color.GreenString(strings.Join(accessibleEndpoints, ", "))})
}
}
}
if isAccessToken {
color.Green("[i] Permissions: Full Access")
} else {
color.Yellow("[i] Permissions:")
}
if !isAccessToken || showAll {
t.SetColumnConfigs([]table.ColumnConfig{
{Number: 2, WidthMax: 100},
})
t.Render()
}
}
func printTeamMembers(team TeamJSON) {
if len(team.TeamMembers) == 0 {
color.Red("\n[x] No team members found")
return
}
color.Yellow("\n[i] Team Members (don't imply any permissions)")
t := table.NewWriter()
t.SetOutputMirror(os.Stdout)
t.AppendHeader(table.Row{"First Name", "Last Name", "Email", "Owner", "Created At"})
for _, member := range team.TeamMembers {
t.AppendRow([]interface{}{color.GreenString(member.FirstName), color.GreenString(member.LastName), color.GreenString(member.Email), color.GreenString(strconv.FormatBool(member.IsOwner)), color.GreenString(member.CreatedAt)})
}
t.Render()
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,300 @@
package stripe
import (
"bytes"
_ "embed"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"os"
"sort"
"strings"
"github.com/fatih/color"
"github.com/jedib0t/go-pretty/table"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/config"
"gopkg.in/yaml.v2"
)
const (
SECRET_PREFIX = "sk_"
PUBLISHABLE_PREFIX = "pk_"
RESTRICTED_PREFIX = "rk_"
LIVE_PREFIX = "live_"
TEST_PREFIX = "test_"
SECRET = "Secret"
PUBLISHABLE = "Publishable"
RESTRICTED = "Restricted"
LIVE = "Live"
TEST = "Test"
)
//go:embed restricted.yaml
var restrictedConfig []byte
type Permission struct {
Name string
Value *string
}
type PermissionsCategory struct {
Name string
Permissions []Permission
}
type HttpStatusTest struct {
Endpoint string `yaml:"Endpoint"`
Method string `yaml:"Method"`
Payload interface{} `yaml:"Payload"`
ValidStatuses []int `yaml:"Valid"`
InvalidStatuses []int `yaml:"Invalid"`
}
type Category map[string]map[string]HttpStatusTest
type Config struct {
Categories map[string]Category `yaml:"categories"`
}
func (h *HttpStatusTest) RunTest(cfg *config.Config, headers map[string]string) (bool, error) {
// If body data, marshal to JSON
var data io.Reader
if h.Payload != nil {
jsonData, err := json.Marshal(h.Payload)
if err != nil {
return false, err
}
data = bytes.NewBuffer(jsonData)
}
// Create new HTTP request
client := analyzers.NewAnalyzeClient(cfg)
req, err := http.NewRequest(h.Method, h.Endpoint, data)
if err != nil {
return false, err
}
// Add custom headers if provided
for key, value := range headers {
req.Header.Set(key, value)
}
// Execute HTTP Request
resp, err := client.Do(req)
if err != nil {
return false, err
}
defer resp.Body.Close()
// Check response status code
switch {
case StatusContains(resp.StatusCode, h.ValidStatuses):
return true, nil
case StatusContains(resp.StatusCode, h.InvalidStatuses):
return false, nil
default:
fmt.Println(h)
fmt.Println(resp.Body)
fmt.Println(resp.StatusCode)
return false, errors.New("error checking response status code")
}
}
func StatusContains(status int, vals []int) bool {
for _, v := range vals {
if status == v {
return true
}
}
return false
}
func checkKeyType(key string) (string, error) {
if strings.HasPrefix(key, SECRET_PREFIX) {
return SECRET, nil
} else if strings.HasPrefix(key, PUBLISHABLE_PREFIX) {
return PUBLISHABLE, nil
} else if strings.HasPrefix(key, RESTRICTED_PREFIX) {
return RESTRICTED, nil
}
return "", errors.New("Invalid Stripe key format")
}
func checkKeyEnv(key string) (string, error) {
//remove first 3 characters
key = key[3:]
if strings.HasPrefix(key, LIVE_PREFIX) {
return LIVE, nil
}
if strings.HasPrefix(key, TEST_PREFIX) {
return TEST, nil
}
return "", errors.New("invalid Stripe key format")
}
func checkValidity(cfg *config.Config, key string) (bool, error) {
// Create a new request
client := analyzers.NewAnalyzeClient(cfg)
req, err := http.NewRequest("GET", "https://api.stripe.com/v1/charges", nil)
if err != nil {
color.Red("[x] Error creating request: %s", err.Error())
return false, err
}
// Add Authorization header
req.Header.Add("Authorization", "Bearer "+key)
// Send the request
resp, err := client.Do(req)
if err != nil {
color.Red("[x] Error sending request: %s", err.Error())
return false, err
}
defer resp.Body.Close()
// Check the response. Valid is 200 (secret/restricted) or 403 (restricted)
if resp.StatusCode == 200 || resp.StatusCode == 403 {
return true, nil
}
return false, nil
}
func AnalyzePermissions(cfg *config.Config, key string) {
// Check if secret, publishable, or restricted key
var keyType, keyEnv string
keyType, err := checkKeyType(key)
if err != nil {
color.Red("[x] ", err.Error())
return
}
if keyType == PUBLISHABLE {
color.Red("[x] This is a publishable Stripe key. It is not considered secret.")
return
}
// Check if live or test key
keyEnv, err = checkKeyEnv(key)
if err != nil {
color.Red("[x] ", err.Error())
return
}
// Check if key is valid
valid, err := checkValidity(cfg, key)
if err != nil {
color.Red("[x] ", err.Error())
return
}
if !valid {
color.Red("[x] Invalid Stripe API Key\n")
return
}
color.Green("[!] Valid Stripe API Key\n\n")
if keyType == SECRET {
color.Green("[i] Key Type: %s", keyType)
} else if keyType == RESTRICTED {
color.Yellow("[i] Key Type: %s", keyType)
}
if keyEnv == LIVE {
color.Green("[i] Key Environment: %s", keyEnv)
} else if keyEnv == TEST {
color.Red("[i] Key Environment: %s", keyEnv)
}
fmt.Println("")
if keyType == SECRET {
color.Green("[i] Permissions: Full Access")
return
}
permissions, err := getRestrictedPermissions(cfg, key)
if err != nil {
color.Red("[x] Error getting permissions: %s", err.Error())
return
}
printRestrictedPermissions(permissions, cfg.ShowAll)
// Additional details
// get total customers
// get total charges
}
func getRestrictedPermissions(cfg *config.Config, key string) ([]PermissionsCategory, error) {
var config Config
if err := yaml.Unmarshal(restrictedConfig, &config); err != nil {
fmt.Println("Error unmarshalling YAML:", err)
return nil, err
}
output := make([]PermissionsCategory, 0)
for category, scopes := range config.Categories {
permissions := make([]Permission, 0)
for name, scope := range scopes {
value := ""
testCount := 0
for typ, test := range scope {
if test.Endpoint == "" {
continue
}
testCount++
status, err := test.RunTest(cfg, map[string]string{"Authorization": "Bearer " + key})
if err != nil {
color.Red("[x] Error running test: %s", err.Error())
return nil, err
}
if status {
value = typ
}
if value == "Write" {
break
}
}
if testCount > 0 {
permissions = append(permissions, Permission{Name: name, Value: &value})
}
}
output = append(output, PermissionsCategory{Name: category, Permissions: permissions})
}
// sort the output
order := []string{"Core", "Checkout", "Billing", "Connect", "Orders", "Issuing", "Reporting", "Identity", "Webhook", "Stripe CLI", "Payment Links", "Terminal", "Tax", "Radar", "Climate"}
// ToDo: order the permissions within each category
// Create a map for quick lookup of the order
orderMap := make(map[string]int)
for i, name := range order {
orderMap[name] = i
}
// Sort the categories according to the desired order
sort.Slice(output, func(i, j int) bool {
return orderMap[output[i].Name] < orderMap[output[j].Name]
})
return output, nil
}
func printRestrictedPermissions(permissions []PermissionsCategory, show_all bool) {
t := table.NewWriter()
t.SetOutputMirror(os.Stdout)
t.AppendHeader(table.Row{"Category", "Permission", "Access"})
for _, category := range permissions {
for _, permission := range category.Permissions {
if *permission.Value != "" || show_all {
t.AppendRow([]interface{}{category.Name, permission.Name, *permission.Value})
}
}
}
t.Render()
}

View file

@ -0,0 +1,160 @@
package twilio
import (
"encoding/json"
"errors"
"fmt"
"net/http"
"strings"
"github.com/fatih/color"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/config"
)
type VerifyJSON struct {
Code int `json:"code"`
}
const (
AUTHENTICATED_NO_PERMISSION = 70051
INVALID_CREDENTIALS = 20003
)
// splitKey splits the key into SID and Secret
func splitKey(key string) (string, string, error) {
split := strings.Split(key, ":")
if len(split) != 2 {
return "", "", errors.New("key must be in the format SID:Secret")
}
return split[0], split[1], nil
}
// getAccountsStatusCode returns the status code from the Accounts endpoint
// this is used to determine whether the key is scoped as main or standard, since standard has no access here.
func getAccountsStatusCode(cfg *config.Config, sid string, secret string) (int, error) {
// create http client
client := analyzers.NewAnalyzeClient(cfg)
// create request
req, err := http.NewRequest("GET", "https://api.twilio.com/2010-04-01/Accounts", nil)
if err != nil {
return 0, err
}
// add query params
q := req.URL.Query()
q.Add("FriendlyName", "zpoOnD08HdLLZGFnGUMTxbX3qQ1kS")
req.URL.RawQuery = q.Encode()
// add basicAuth
req.SetBasicAuth(sid, secret)
// send request
resp, err := client.Do(req)
if err != nil {
return 0, err
}
defer resp.Body.Close()
return resp.StatusCode, nil
}
// getVerifyServicesStatusCode returns the status code and the JSON response from the Verify Services endpoint
// only the code value is captured in the JSON response and this is only shown when the key is invalid or has no permissions
func getVerifyServicesStatusCode(cfg *config.Config, sid string, secret string) (VerifyJSON, error) {
var verifyJSON VerifyJSON
// create http client
client := analyzers.NewAnalyzeClient(cfg)
// create request
req, err := http.NewRequest("GET", "https://verify.twilio.com/v2/Services", nil)
if err != nil {
return verifyJSON, err
}
// add query params
q := req.URL.Query()
q.Add("FriendlyName", "zpoOnD08HdLLZGFnGUMTxbX3qQ1kS")
req.URL.RawQuery = q.Encode()
// add basicAuth
req.SetBasicAuth(sid, secret)
// send request
resp, err := client.Do(req)
if err != nil {
return verifyJSON, err
}
defer resp.Body.Close()
// read response
if err := json.NewDecoder(resp.Body).Decode(&verifyJSON); err != nil {
return verifyJSON, err
}
return verifyJSON, nil
}
func AnalyzePermissions(cfg *config.Config, key string) {
sid, secret, err := splitKey(key)
if err != nil {
color.Red("[x]" + err.Error())
return
}
verifyJSON, err := getVerifyServicesStatusCode(cfg, sid, secret)
if err != nil {
color.Red("[x]" + err.Error())
return
}
if verifyJSON.Code == INVALID_CREDENTIALS {
color.Red("[x] Invalid Twilio API Key")
return
}
if verifyJSON.Code == AUTHENTICATED_NO_PERMISSION {
printRestrictedKeyMsg()
return
}
statusCode, err := getAccountsStatusCode(cfg, sid, secret)
if err != nil {
color.Red("[x]" + err.Error())
return
}
printPermissions(statusCode)
}
// printPermissions prints the permissions based on the status code
// 200 means the key is main, 401 means the key is standard
func printPermissions(statusCode int) {
if statusCode != 200 && statusCode != 401 {
color.Red("[x] Invalid Twilio API Key")
return
}
color.Green("[!] Valid Twilio API Key\n")
color.Green("[i] Expires: Never")
if statusCode == 401 {
color.Yellow("[i] Key type: Standard")
color.Yellow("[i] Permissions: All EXCEPT key management and account/subaccount configuration.")
} else if statusCode == 200 {
color.Green("[i] Key type: Main (aka Admin)")
color.Green("[i] Permissions: All")
}
}
// printRestrictedKeyMsg prints the message for a restricted key
// this is a temporary measure since the restricted key type is still in beta
func printRestrictedKeyMsg() {
color.Green("[!] Valid Twilio API Key\n")
color.Green("[i] Expires: Never")
color.Yellow("[i] Key type: Restricted")
color.Yellow("[i] Permissions: Limited")
fmt.Println("[*] Note: Twilio is rolling out a Restricted API Key type, which provides fine-grained control over API endpoints. Since it's still in a Public Beta, this has not been incorporated into this tool.")
}

250
pkg/analyzer/cli.go Normal file
View file

@ -0,0 +1,250 @@
package analyzer
import (
"github.com/alecthomas/kingpin/v2"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers/airbrake"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers/asana"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers/bitbucket"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers/github"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers/gitlab"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers/huggingface"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers/mailchimp"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers/mailgun"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers/mysql"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers/openai"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers/opsgenie"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers/postgres"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers/postman"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers/sendgrid"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers/shopify"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers/slack"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers/sourcegraph"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers/square"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers/stripe"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers/twilio"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/config"
)
var (
// TODO: Add list of supported key types.
list *kingpin.CmdClause
showAll *bool
log *bool
githubScan *kingpin.CmdClause
githubKey *string
sendgridScan *kingpin.CmdClause
sendgridKey *string
openAIScan *kingpin.CmdClause
openaiKey *string
postgresScan *kingpin.CmdClause
postgresConnectionStr *string
mysqlScan *kingpin.CmdClause
mysqlConnectionStr *string
// mongodbScan *kingpin.CmdClause
// mongodbConnectionStr *string
slackScan *kingpin.CmdClause
slackKey *string
twilioScan *kingpin.CmdClause
twilioKey *string
airbrakeScan *kingpin.CmdClause
airbrakeKey *string
huggingfaceScan *kingpin.CmdClause
huggingfaceKey *string
stripeScan *kingpin.CmdClause
stripeKey *string
gitlabScan *kingpin.CmdClause
gitlabKey *string
mailchimpScan *kingpin.CmdClause
mailchimpKey *string
// mandrillScan *kingpin.CmdClause
// mandrillKey *string
postmanScan *kingpin.CmdClause
postmanKey *string
bitbucketScan *kingpin.CmdClause
bitbucketKey *string
asanaScan *kingpin.CmdClause
asanaKey *string
mailgunScan *kingpin.CmdClause
mailgunKey *string
squareScan *kingpin.CmdClause
squareKey *string
sourcegraphScan *kingpin.CmdClause
sourcegraphKey *string
shopifyScan *kingpin.CmdClause
shopifyKey *string
shopifyStoreURL *string
opsgenieScan *kingpin.CmdClause
opsgenieKey *string
)
func Command(app *kingpin.Application) *kingpin.CmdClause {
// TODO: Add list of supported key types.
cli := app.Command("analyze", "Analyze API keys for fine-grained permissions information").Hidden()
list = cli.Command("list", "List supported API providers")
showAll = cli.Flag("show-all", "Show all data, including permissions not available to this account + publicly-available data related to this account.").Default("false").Bool()
log = cli.Flag("log", "Log all HTTP requests sent during analysis to a file").Default("false").Bool()
githubScan = cli.Command("github", "Scan a GitHub API key")
githubKey = githubScan.Arg("key", "GitHub Key.").Required().String()
sendgridScan = cli.Command("sendgrid", "Scan a Sendgrid API key")
sendgridKey = sendgridScan.Arg("key", "Sendgrid Key.").Required().String()
openAIScan = cli.Command("openai", "Scan an OpenAI API key")
openaiKey = openAIScan.Arg("key", "OpenAI Key.").Required().String()
postgresScan = cli.Command("postgres", "Scan a Postgres connection string")
postgresConnectionStr = postgresScan.Arg("connection-string", "Postgres Connection String. As a reference, here's an example: postgresql://[user[:password]@][netloc][:port][/dbname][?param1=value1&...]").Required().String()
mysqlScan = cli.Command("mysql", "Scan a MySQL connection string")
mysqlConnectionStr = mysqlScan.Arg("connection-string", "MySQL Connection String. As a reference, here's an example: mysql://[user[:password]@][netloc][:port][/dbname][?param1=value1&...]").Required().String()
// mongodbScan = cli.Command("mongodb", "Scan a MongoDB connection string")
// mongodbConnectionStr = mongodbScan.Arg("connection-string", "MongoDB Connection String. As a reference, here's an example: mongodb://[username:password@]host1[:port1][,host2[:port2],...[,hostN[:portN]]][/[defaultauthdb][?options]]").Required().String()
slackScan = cli.Command("slack", "Scan a Slack API key")
slackKey = slackScan.Arg("key", "Slack Key.").Required().String()
twilioScan = cli.Command("twilio", "Scan a Twilio API key")
twilioKey = twilioScan.Arg("key", "Twilio API Key SID & Secret (ex: keySID:keySecret).").Required().String()
airbrakeScan = cli.Command("airbrake", "Scan an Airbrake User Key or Token")
airbrakeKey = airbrakeScan.Arg("key", "Airbrake User Key or Token.").Required().String()
huggingfaceScan = cli.Command("huggingface", "Scan a Huggingface API key")
huggingfaceKey = huggingfaceScan.Arg("key", "Huggingface Key.").Required().String()
stripeScan = cli.Command("stripe", "Scan a Stripe API key")
stripeKey = stripeScan.Arg("key", "Stripe Key.").Required().String()
gitlabScan = cli.Command("gitlab", "Scan a GitLab API key")
gitlabKey = gitlabScan.Arg("key", "GitLab Key.").Required().String()
mailchimpScan = cli.Command("mailchimp", "Scan a Mailchimp API key")
mailchimpKey = mailchimpScan.Arg("key", "Mailchimp Key.").Required().String()
// mandrillScan = cli.Command("mandrill", "Scan a Mandrill API key")
// mandrillKey = mandrillScan.Arg("key", "Mandril Key.").Required().String()
postmanScan = cli.Command("postman", "Scan a Postman API key")
postmanKey = postmanScan.Arg("key", "Postman Key.").Required().String()
bitbucketScan = cli.Command("bitbucket", "Scan a Bitbucket Access Token")
bitbucketKey = bitbucketScan.Arg("key", "Bitbucket Access Token.").Required().String()
asanaScan = cli.Command("asana", "Scan an Asana API key")
asanaKey = asanaScan.Arg("key", "Asana Key.").Required().String()
mailgunScan = cli.Command("mailgun", "Scan a Mailgun API key")
mailgunKey = mailgunScan.Arg("key", "Mailgun Key.").Required().String()
squareScan = cli.Command("square", "Scan a Square API key")
squareKey = squareScan.Arg("key", "Square Key.").Required().String()
sourcegraphScan = cli.Command("sourcegraph", "Scan a Sourcegraph Access Token")
sourcegraphKey = sourcegraphScan.Arg("key", "Sourcegraph Access Token.").Required().String()
shopifyScan = cli.Command("shopify", "Scan a Shopify API key")
shopifyKey = shopifyScan.Arg("key", "Shopify Key.").Required().String()
shopifyStoreURL = shopifyScan.Arg("store-url", "Shopify Store Domain (ex: 22297c-c6.myshopify.com).").Required().String()
opsgenieScan = cli.Command("opsgenie", "Scan an Opsgenie API key")
opsgenieKey = opsgenieScan.Arg("key", "Opsgenie Key.").Required().String()
return cli
}
func Run(cmd string) {
// Initialize configuration
cfg := &config.Config{
LoggingEnabled: *log,
ShowAll: *showAll,
}
switch cmd {
case list.FullCommand():
panic("todo")
case githubScan.FullCommand():
cfg.LogFile = analyzers.CreateLogFileName("github")
github.AnalyzePermissions(cfg, *githubKey)
case sendgridScan.FullCommand():
cfg.LogFile = analyzers.CreateLogFileName("sendgrid")
sendgrid.AnalyzePermissions(cfg, *sendgridKey)
case openAIScan.FullCommand():
cfg.LogFile = analyzers.CreateLogFileName("openai")
openai.AnalyzePermissions(cfg, *openaiKey)
case postgresScan.FullCommand():
cfg.LogFile = analyzers.CreateLogFileName("postgres")
postgres.AnalyzePermissions(cfg, *postgresConnectionStr)
case mysqlScan.FullCommand():
cfg.LogFile = analyzers.CreateLogFileName("mysql")
mysql.AnalyzePermissions(cfg, *mysqlConnectionStr)
case slackScan.FullCommand():
cfg.LogFile = analyzers.CreateLogFileName("slack")
slack.AnalyzePermissions(cfg, *slackKey)
case twilioScan.FullCommand():
cfg.LogFile = analyzers.CreateLogFileName("twilio")
twilio.AnalyzePermissions(cfg, *twilioKey)
case airbrakeScan.FullCommand():
cfg.LogFile = analyzers.CreateLogFileName("airbrake")
airbrake.AnalyzePermissions(cfg, *airbrakeKey)
case huggingfaceScan.FullCommand():
cfg.LogFile = analyzers.CreateLogFileName("huggingface")
huggingface.AnalyzePermissions(cfg, *huggingfaceKey)
case stripeScan.FullCommand():
cfg.LogFile = analyzers.CreateLogFileName("stripe")
stripe.AnalyzePermissions(cfg, *stripeKey)
case gitlabScan.FullCommand():
cfg.LogFile = analyzers.CreateLogFileName("gitlab")
gitlab.AnalyzePermissions(cfg, *gitlabKey)
case mailchimpScan.FullCommand():
cfg.LogFile = analyzers.CreateLogFileName("mailchimp")
mailchimp.AnalyzePermissions(cfg, *mailchimpKey)
case postmanScan.FullCommand():
cfg.LogFile = analyzers.CreateLogFileName("postman")
postman.AnalyzePermissions(cfg, *postmanKey)
case bitbucketScan.FullCommand():
cfg.LogFile = analyzers.CreateLogFileName("bitbucket")
bitbucket.AnalyzePermissions(cfg, *bitbucketKey)
case asanaScan.FullCommand():
cfg.LogFile = analyzers.CreateLogFileName("asana")
asana.AnalyzePermissions(cfg, *asanaKey)
case mailgunScan.FullCommand():
cfg.LogFile = analyzers.CreateLogFileName("mailgun")
mailgun.AnalyzePermissions(cfg, *mailgunKey)
case squareScan.FullCommand():
cfg.LogFile = analyzers.CreateLogFileName("square")
square.AnalyzePermissions(cfg, *squareKey)
case sourcegraphScan.FullCommand():
cfg.LogFile = analyzers.CreateLogFileName("sourcegraph")
sourcegraph.AnalyzePermissions(cfg, *sourcegraphKey)
case shopifyScan.FullCommand():
cfg.LogFile = analyzers.CreateLogFileName("shopify")
shopify.AnalyzePermissions(cfg, *shopifyKey, *shopifyStoreURL)
case opsgenieScan.FullCommand():
cfg.LogFile = analyzers.CreateLogFileName("opsgenie")
opsgenie.AnalyzePermissions(cfg, *opsgenieKey)
}
}

View file

@ -0,0 +1,8 @@
package config
// TODO: separate CLI configuration from analysis configuration.
type Config struct {
LoggingEnabled bool
LogFile string
ShowAll bool
}

View file

@ -0,0 +1,203 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.33.0
// protoc v4.25.3
// source: analyzer.proto
package analyzerpb
import (
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
reflect "reflect"
sync "sync"
)
const (
// Verify that this generated code is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
// Verify that runtime/protoimpl is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
)
type AnalyzerType int32
const (
AnalyzerType_Invalid AnalyzerType = 0
AnalyzerType_Airbrake AnalyzerType = 1
AnalyzerType_Asana AnalyzerType = 2
AnalyzerType_Bitbucket AnalyzerType = 3
AnalyzerType_GitHub AnalyzerType = 4
AnalyzerType_GitLab AnalyzerType = 5
AnalyzerType_HuggingFace AnalyzerType = 6
AnalyzerType_Mailchimp AnalyzerType = 7
AnalyzerType_Mailgun AnalyzerType = 8
AnalyzerType_MySQL AnalyzerType = 9
AnalyzerType_OpenAI AnalyzerType = 10
AnalyzerType_Opsgenie AnalyzerType = 11
AnalyzerType_Postgres AnalyzerType = 12
AnalyzerType_Postman AnalyzerType = 13
AnalyzerType_Sendgrid AnalyzerType = 14
AnalyzerType_Shopify AnalyzerType = 15
AnalyzerType_Slack AnalyzerType = 16
AnalyzerType_Sourcegraph AnalyzerType = 17
AnalyzerType_Square AnalyzerType = 18
AnalyzerType_Stripe AnalyzerType = 19
AnalyzerType_Twilio AnalyzerType = 20
)
// Enum value maps for AnalyzerType.
var (
AnalyzerType_name = map[int32]string{
0: "Invalid",
1: "Airbrake",
2: "Asana",
3: "Bitbucket",
4: "GitHub",
5: "GitLab",
6: "HuggingFace",
7: "Mailchimp",
8: "Mailgun",
9: "MySQL",
10: "OpenAI",
11: "Opsgenie",
12: "Postgres",
13: "Postman",
14: "Sendgrid",
15: "Shopify",
16: "Slack",
17: "Sourcegraph",
18: "Square",
19: "Stripe",
20: "Twilio",
}
AnalyzerType_value = map[string]int32{
"Invalid": 0,
"Airbrake": 1,
"Asana": 2,
"Bitbucket": 3,
"GitHub": 4,
"GitLab": 5,
"HuggingFace": 6,
"Mailchimp": 7,
"Mailgun": 8,
"MySQL": 9,
"OpenAI": 10,
"Opsgenie": 11,
"Postgres": 12,
"Postman": 13,
"Sendgrid": 14,
"Shopify": 15,
"Slack": 16,
"Sourcegraph": 17,
"Square": 18,
"Stripe": 19,
"Twilio": 20,
}
)
func (x AnalyzerType) Enum() *AnalyzerType {
p := new(AnalyzerType)
*p = x
return p
}
func (x AnalyzerType) String() string {
return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))
}
func (AnalyzerType) Descriptor() protoreflect.EnumDescriptor {
return file_analyzer_proto_enumTypes[0].Descriptor()
}
func (AnalyzerType) Type() protoreflect.EnumType {
return &file_analyzer_proto_enumTypes[0]
}
func (x AnalyzerType) Number() protoreflect.EnumNumber {
return protoreflect.EnumNumber(x)
}
// Deprecated: Use AnalyzerType.Descriptor instead.
func (AnalyzerType) EnumDescriptor() ([]byte, []int) {
return file_analyzer_proto_rawDescGZIP(), []int{0}
}
var File_analyzer_proto protoreflect.FileDescriptor
var file_analyzer_proto_rawDesc = []byte{
0x0a, 0x0e, 0x61, 0x6e, 0x61, 0x6c, 0x79, 0x7a, 0x65, 0x72, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f,
0x12, 0x08, 0x61, 0x6e, 0x61, 0x6c, 0x79, 0x7a, 0x65, 0x72, 0x2a, 0xa3, 0x02, 0x0a, 0x0c, 0x41,
0x6e, 0x61, 0x6c, 0x79, 0x7a, 0x65, 0x72, 0x54, 0x79, 0x70, 0x65, 0x12, 0x0b, 0x0a, 0x07, 0x49,
0x6e, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x10, 0x00, 0x12, 0x0c, 0x0a, 0x08, 0x41, 0x69, 0x72, 0x62,
0x72, 0x61, 0x6b, 0x65, 0x10, 0x01, 0x12, 0x09, 0x0a, 0x05, 0x41, 0x73, 0x61, 0x6e, 0x61, 0x10,
0x02, 0x12, 0x0d, 0x0a, 0x09, 0x42, 0x69, 0x74, 0x62, 0x75, 0x63, 0x6b, 0x65, 0x74, 0x10, 0x03,
0x12, 0x0a, 0x0a, 0x06, 0x47, 0x69, 0x74, 0x48, 0x75, 0x62, 0x10, 0x04, 0x12, 0x0a, 0x0a, 0x06,
0x47, 0x69, 0x74, 0x4c, 0x61, 0x62, 0x10, 0x05, 0x12, 0x0f, 0x0a, 0x0b, 0x48, 0x75, 0x67, 0x67,
0x69, 0x6e, 0x67, 0x46, 0x61, 0x63, 0x65, 0x10, 0x06, 0x12, 0x0d, 0x0a, 0x09, 0x4d, 0x61, 0x69,
0x6c, 0x63, 0x68, 0x69, 0x6d, 0x70, 0x10, 0x07, 0x12, 0x0b, 0x0a, 0x07, 0x4d, 0x61, 0x69, 0x6c,
0x67, 0x75, 0x6e, 0x10, 0x08, 0x12, 0x09, 0x0a, 0x05, 0x4d, 0x79, 0x53, 0x51, 0x4c, 0x10, 0x09,
0x12, 0x0a, 0x0a, 0x06, 0x4f, 0x70, 0x65, 0x6e, 0x41, 0x49, 0x10, 0x0a, 0x12, 0x0c, 0x0a, 0x08,
0x4f, 0x70, 0x73, 0x67, 0x65, 0x6e, 0x69, 0x65, 0x10, 0x0b, 0x12, 0x0c, 0x0a, 0x08, 0x50, 0x6f,
0x73, 0x74, 0x67, 0x72, 0x65, 0x73, 0x10, 0x0c, 0x12, 0x0b, 0x0a, 0x07, 0x50, 0x6f, 0x73, 0x74,
0x6d, 0x61, 0x6e, 0x10, 0x0d, 0x12, 0x0c, 0x0a, 0x08, 0x53, 0x65, 0x6e, 0x64, 0x67, 0x72, 0x69,
0x64, 0x10, 0x0e, 0x12, 0x0b, 0x0a, 0x07, 0x53, 0x68, 0x6f, 0x70, 0x69, 0x66, 0x79, 0x10, 0x0f,
0x12, 0x09, 0x0a, 0x05, 0x53, 0x6c, 0x61, 0x63, 0x6b, 0x10, 0x10, 0x12, 0x0f, 0x0a, 0x0b, 0x53,
0x6f, 0x75, 0x72, 0x63, 0x65, 0x67, 0x72, 0x61, 0x70, 0x68, 0x10, 0x11, 0x12, 0x0a, 0x0a, 0x06,
0x53, 0x71, 0x75, 0x61, 0x72, 0x65, 0x10, 0x12, 0x12, 0x0a, 0x0a, 0x06, 0x53, 0x74, 0x72, 0x69,
0x70, 0x65, 0x10, 0x13, 0x12, 0x0a, 0x0a, 0x06, 0x54, 0x77, 0x69, 0x6c, 0x69, 0x6f, 0x10, 0x14,
0x42, 0x45, 0x5a, 0x43, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x74,
0x72, 0x75, 0x66, 0x66, 0x6c, 0x65, 0x73, 0x65, 0x63, 0x75, 0x72, 0x69, 0x74, 0x79, 0x2f, 0x74,
0x72, 0x75, 0x66, 0x66, 0x6c, 0x65, 0x68, 0x6f, 0x67, 0x2f, 0x76, 0x33, 0x2f, 0x70, 0x6b, 0x67,
0x2f, 0x61, 0x6e, 0x61, 0x6c, 0x79, 0x7a, 0x65, 0x72, 0x2f, 0x70, 0x62, 0x2f, 0x61, 0x6e, 0x61,
0x6c, 0x79, 0x7a, 0x65, 0x72, 0x70, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
}
var (
file_analyzer_proto_rawDescOnce sync.Once
file_analyzer_proto_rawDescData = file_analyzer_proto_rawDesc
)
func file_analyzer_proto_rawDescGZIP() []byte {
file_analyzer_proto_rawDescOnce.Do(func() {
file_analyzer_proto_rawDescData = protoimpl.X.CompressGZIP(file_analyzer_proto_rawDescData)
})
return file_analyzer_proto_rawDescData
}
var file_analyzer_proto_enumTypes = make([]protoimpl.EnumInfo, 1)
var file_analyzer_proto_goTypes = []interface{}{
(AnalyzerType)(0), // 0: analyzer.AnalyzerType
}
var file_analyzer_proto_depIdxs = []int32{
0, // [0:0] is the sub-list for method output_type
0, // [0:0] is the sub-list for method input_type
0, // [0:0] is the sub-list for extension type_name
0, // [0:0] is the sub-list for extension extendee
0, // [0:0] is the sub-list for field type_name
}
func init() { file_analyzer_proto_init() }
func file_analyzer_proto_init() {
if File_analyzer_proto != nil {
return
}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: file_analyzer_proto_rawDesc,
NumEnums: 1,
NumMessages: 0,
NumExtensions: 0,
NumServices: 0,
},
GoTypes: file_analyzer_proto_goTypes,
DependencyIndexes: file_analyzer_proto_depIdxs,
EnumInfos: file_analyzer_proto_enumTypes,
}.Build()
File_analyzer_proto = out.File
file_analyzer_proto_rawDesc = nil
file_analyzer_proto_goTypes = nil
file_analyzer_proto_depIdxs = nil
}

View file

@ -0,0 +1,36 @@
// Code generated by protoc-gen-validate. DO NOT EDIT.
// source: analyzer.proto
package analyzerpb
import (
"bytes"
"errors"
"fmt"
"net"
"net/mail"
"net/url"
"regexp"
"sort"
"strings"
"time"
"unicode/utf8"
"google.golang.org/protobuf/types/known/anypb"
)
// ensure the imports are used
var (
_ = bytes.MinRead
_ = errors.New("")
_ = fmt.Print
_ = utf8.UTFMax
_ = (*regexp.Regexp)(nil)
_ = (*strings.Reader)(nil)
_ = net.IPv4len
_ = time.Duration(0)
_ = (*url.URL)(nil)
_ = (*mail.Address)(nil)
_ = anypb.Any{}
_ = sort.Sort
)

View file

@ -0,0 +1,29 @@
syntax = "proto3";
package analyzer;
option go_package = "github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/pb/analyzerpb";
enum AnalyzerType {
Invalid = 0;
Airbrake = 1;
Asana = 2;
Bitbucket = 3;
GitHub = 4;
GitLab = 5;
HuggingFace = 6;
Mailchimp = 7;
Mailgun = 8;
MySQL = 9;
OpenAI = 10;
Opsgenie = 11;
Postgres = 12;
Postman = 13;
Sendgrid = 14;
Shopify = 15;
Slack = 16;
Sourcegraph = 17;
Square = 18;
Stripe = 19;
Twilio = 20;
}

View file

@ -82,6 +82,11 @@ type Result struct {
// This field should only be populated if the verification process itself failed in a way that provides no
// information about the verification status of the candidate secret, such as if the verification request timed out.
verificationError error
// AnalysisInfo should be set with information required for credential
// analysis to run. The keys of the map are analyzer specific and
// should match what is expected in the corresponding analyzer.
AnalysisInfo map[string]string
}
// SetVerificationError is the only way to set a verification error. Any sensitive values should be passed-in as secrets to be redacted.

View file

@ -62,6 +62,7 @@ func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (result
s1.Verified = verified
s1.ExtraData = extraData
s1.SetVerificationError(verificationErr)
s1.AnalysisInfo = map[string]string{"key": token}
}
results = append(results, s1)

View file

@ -2,38 +2,25 @@
set -eux
protoc -I proto/ \
-I ${GOPATH}/src \
-I /usr/local/include \
-I ${GOPATH}/src/github.com/envoyproxy/protoc-gen-validate \
--go_out=plugins=grpc:./pkg/pb/credentialspb --go_opt=paths=source_relative \
--validate_out="lang=go,paths=source_relative:./pkg/pb/credentialspb" \
proto/credentials.proto
protoc -I proto/ \
-I ${GOPATH}/src \
-I /usr/local/include \
-I ${GOPATH}/src/github.com/envoyproxy/protoc-gen-validate \
--go_out=plugins=grpc:./pkg/pb/sourcespb --go_opt=paths=source_relative \
--validate_out="lang=go,paths=source_relative:./pkg/pb/sourcespb" \
proto/sources.proto
protoc -I proto/ \
-I ${GOPATH}/src \
-I /usr/local/include \
-I ${GOPATH}/src/github.com/envoyproxy/protoc-gen-validate \
--go_out=plugins=grpc:./pkg/pb/detectorspb --go_opt=paths=source_relative \
--validate_out="lang=go,paths=source_relative:./pkg/pb/detectorspb" \
proto/detectors.proto
protoc -I proto/ \
-I ${GOPATH}/src \
-I /usr/local/include \
-I ${GOPATH}/src/github.com/envoyproxy/protoc-gen-validate \
--go_out=plugins=grpc:./pkg/pb/source_metadatapb --go_opt=paths=source_relative \
--validate_out="lang=go,paths=source_relative:./pkg/pb/source_metadatapb" \
proto/source_metadata.proto
protoc -I proto/ \
-I ${GOPATH}/src \
-I /usr/local/include \
-I ${GOPATH}/src/github.com/envoyproxy/protoc-gen-validate \
--go_out=plugins=grpc:./pkg/pb/custom_detectorspb --go_opt=paths=source_relative \
--validate_out="lang=go,paths=source_relative:./pkg/pb/custom_detectorspb" \
proto/custom_detectors.proto
for pbfile in $(ls proto/); do
mod=${pbfile%%.proto}
protoc -I proto/ \
-I ${GOPATH}/src \
-I /usr/local/include \
-I ${GOPATH}/src/github.com/envoyproxy/protoc-gen-validate \
--go_out=plugins=grpc:./pkg/pb/${mod}pb --go_opt=paths=source_relative \
--validate_out="lang=go,paths=source_relative:./pkg/pb/${mod}pb" \
proto/${mod}.proto
done
for pbfile in $(ls pkg/analyzer/proto/); do
mod=${pbfile%%.proto}
mkdir -p "./pkg/analyzer/pb/${mod}pb"
protoc -I pkg/analyzer/proto/ \
-I ${GOPATH}/src \
-I /usr/local/include \
-I ${GOPATH}/src/github.com/envoyproxy/protoc-gen-validate \
--go_out=plugins=grpc:./pkg/analyzer/pb/${mod}pb --go_opt=paths=source_relative \
--validate_out="lang=go,paths=source_relative:./pkg/analyzer/pb/${mod}pb" \
pkg/analyzer/proto/${mod}.proto
done