From 46e4f0dfac2e5c0a9b86311684d63b851a47ad96 Mon Sep 17 00:00:00 2001 From: rwxd Date: Thu, 12 Jun 2025 17:53:57 +0200 Subject: [PATCH] feat: json output & coloring table --- README.md | 26 +++++++++++++- cmd/root.go | 62 +++++++++++++++++++++----------- go.mod | 9 ++--- go.sum | 77 ++++++++++----------------------------- misc/misc.go | 100 ++++++++++++++++++++++++++++++++++++++++++++++++--- pdns/pdns.go | 25 +++++++++---- 6 files changed, 201 insertions(+), 98 deletions(-) diff --git a/README.md b/README.md index 581362d..fe54e31 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,6 @@ Under [Releases](https://github.com/akquinet/pdnsgrep/releases) the binary can b Alternatively, you can install it with go: `go install github.com/akquinet/pdnsgrep@latest` - ## Configuration ### Environment variables @@ -76,12 +75,20 @@ example.domain. lab-asa-01.example.domain. AAAA [IPv6 Address] 3600 example.domain. lab-asa-02.example.domain. AAAA [IPv6 Address] 3600 ``` +### Disable colored output + +```bash +❯ pdnsgrep "fw" --no-color +``` + ### Piping into less + ```bash ❯ pdnsgrep "*firewall*" | less -S ``` ### Get only the names + ```bash ❯ pdnsgrep "fw" --output raw --no-header | cut -d' ' -f 2 | sort -u fw-01.example.domain @@ -97,3 +104,20 @@ example.domain.;fw-1.example.domain.;AAAA;IPv6 Address;3600;record example.domain.;fw-1.example.domain.;A;IPv4 Address;3600;record .... ``` + +### JSON Export + +```bash +❯ pdnsgrep "fw" --output json +[ + { + "name": "fw-1.example.domain.", + "type": "A", + "content": "[IPv4 Address]", + "object_type": "record", + "zone": "example.domain.", + "ttl": 3600 + }, + ... +] +``` diff --git a/cmd/root.go b/cmd/root.go index e5a2122..9d948fe 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "os" + "time" "github.com/akquinet/pdnsgrep/misc" "github.com/akquinet/pdnsgrep/pdns" @@ -16,9 +17,11 @@ import ( // rootCmd represents the base command when called without any subcommands var rootCmd = &cobra.Command{ - Use: "pdnsgrep (SEARCH [NAME])", - Short: "Search blazingly fast trough PowerDNS Entries", - Example: "pdnsgrep \"*firewall*\"", + Use: "pdnsgrep SEARCH [SEARCH...]", + Short: "Search blazingly fast trough PowerDNS Entries", + Example: "pdnsgrep \"*firewall*\"", + DisableFlagsInUseLine: true, + Args: cobra.MinimumNArgs(1), Run: func(cmd *cobra.Command, args []string) { completion := viper.GetString("show-completion") if completion != "" { @@ -27,12 +30,9 @@ var rootCmd = &cobra.Command{ } initConfig() - if len(args) == 0 { - fmt.Println("missing search term") - os.Exit(1) - } + // We don't need to check for empty args anymore since we've set MinimumNArgs(1) - client := pdns.NewPDNSAPI(viper.GetString("url"), viper.GetString("token")) + client := createPDNSClient() objectType := "all" if viper.GetBool("zone") { @@ -58,21 +58,39 @@ var rootCmd = &cobra.Command{ os.Exit(0) } - switch viper.GetString("output") { - case "table": - misc.OutputToTable(found) - case "csv": - misc.OutputToCSV(found, viper.GetString("delimiter")) - case "raw": - misc.OutputToStdout(found) - default: - log.Errorf("Output format %s not known\n", viper.GetString("output")) - log.Exit(1) - } - + outputResults(found) }, } +func outputResults(records []pdns.PDNSSearchResponseItem) { + switch viper.GetString("output") { + case "table": + misc.OutputToTable(records) + case "csv": + misc.OutputToCSV(records, viper.GetString("delimiter")) + case "raw": + misc.OutputToStdout(records) + case "json": + misc.OutputToJSON(records) + default: + log.Errorf("Output format %s not known\n", viper.GetString("output")) + log.Exit(1) + } +} + +func createPDNSClient() *pdns.PDNSAPI { + client := pdns.NewPDNSAPI(viper.GetString("url"), viper.GetString("token")) + + // Set timeout if specified + if viper.IsSet("timeout") { + timeout := time.Duration(viper.GetInt("timeout")) * time.Second + client.Timeout = timeout + client.Client.Timeout = timeout + } + + return client +} + func ShowCompletions(cmd *cobra.Command, shell string) { switch shell { case "bash": @@ -145,13 +163,15 @@ func init() { rootCmd.Flags().StringP("config", "c", "", "path to a config file") rootCmd.Flags().String("token", "", "PowerDNS Token") rootCmd.Flags().StringP("url", "u", "", "PowerDNS API URL") - rootCmd.Flags().StringP("output", "o", "table", "output (table|csv|raw)") + rootCmd.Flags().StringP("output", "o", "table", "output (table|csv|raw|json)") rootCmd.Flags().String("delimiter", ";", "Delimiter when csv export is used") rootCmd.Flags().Bool("no-header", false, "do not show header in output") + rootCmd.Flags().Bool("no-color", false, "disable colored output") rootCmd.Flags().Bool("zone", false, "search only for zones") rootCmd.Flags().Bool("record", false, "search only for records") rootCmd.Flags().Bool("comment", false, "search only for comments") rootCmd.Flags().StringP("type", "t", "", "filter type of record (A, AAAA, TXT ....)") + rootCmd.Flags().IntP("timeout", "", 10, "timeout in seconds for API requests") rootCmd.Flags().String("show-completion", "", "show completion (bash, zsh, fish, powershell)") viper.AutomaticEnv() diff --git a/go.mod b/go.mod index 898bd26..e3f2298 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.23.0 toolchain go1.24.2 require ( + github.com/fatih/color v1.18.0 github.com/mitchellh/go-homedir v1.1.0 github.com/sirupsen/logrus v1.9.3 github.com/spf13/cobra v1.9.1 @@ -15,14 +16,12 @@ require ( require ( github.com/fsnotify/fsnotify v1.8.0 // indirect github.com/go-viper/mapstructure/v2 v2.2.1 // indirect - github.com/hashicorp/hcl v1.0.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect - github.com/magiconair/properties v1.8.7 // indirect - github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect github.com/pelletier/go-toml/v2 v2.2.3 // indirect github.com/sagikazarmark/locafero v0.7.0 // indirect - github.com/sagikazarmark/slog-shim v0.1.0 // indirect github.com/sourcegraph/conc v0.3.0 // indirect github.com/spf13/afero v1.12.0 // indirect github.com/spf13/cast v1.7.1 // indirect @@ -30,10 +29,8 @@ require ( github.com/subosito/gotenv v1.6.0 // indirect go.uber.org/atomic v1.9.0 // indirect go.uber.org/multierr v1.9.0 // indirect - golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect golang.org/x/sys v0.29.0 // indirect golang.org/x/text v0.21.0 // indirect gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect - gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 0b3207d..b9ca0b4 100644 --- a/go.sum +++ b/go.sum @@ -1,119 +1,80 @@ -github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= +github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= -github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= -github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= +github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M= github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss= github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= -github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= -github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= -github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +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/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= -github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= -github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= -github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= -github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= -github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= -github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M= github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ= -github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4= github.com/sagikazarmark/locafero v0.7.0 h1:5MqpDsTGNDhY8sGp0Aowyf0qKsPrhewaLSsFaodPcyo= github.com/sagikazarmark/locafero v0.7.0/go.mod h1:2za3Cg5rMaTMoG/2Ulr9AwtFaIppKXTRYnozin4aB5k= -github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= -github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= -github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= -github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= github.com/spf13/afero v1.12.0 h1:UcOPyRBYczmFn6yvphxkn9ZEOY65cpwGKb5mL36mrqs= github.com/spf13/afero v1.12.0/go.mod h1:ZTlWwG4/ahT8W7T0WQ5uYmjI9duaLQGy3Q2OAl4sk/4= -github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0= -github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y= github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= -github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= -github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= -github.com/spf13/cobra v1.9.0 h1:Py5fIuq/lJsRYxcxfOtsJqpmwJWCMOUy2tMJYV8TNHE= -github.com/spf13/cobra v1.9.0/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0= github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo= github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0= -github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= -github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI= -github.com/spf13/viper v1.19.0/go.mod h1:GQUN9bilAbhU/jgc1bKs99f/suXKeUMct8Adx5+Ntkg= -github.com/spf13/viper v1.20.0 h1:zrxIyR3RQIOsarIrgL8+sAvALXul9jeEPa06Y0Ph6vY= -github.com/spf13/viper v1.20.0/go.mod h1:P9Mdzt1zoHIG8m2eZQinpiBjo6kCmZSKBClNNqjJvu4= github.com/spf13/viper v1.20.1 h1:ZMi+z/lvLyPSCoNtFCpqjy0S4kPbirhpTMwl8BkW9X4= github.com/spf13/viper v1.20.1/go.mod h1:P9Mdzt1zoHIG8m2eZQinpiBjo6kCmZSKBClNNqjJvu4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= -github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= -github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI= go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ= -golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g= -golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k= -golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= -golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= -golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= -golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w= -golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw= -golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610= golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= -golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= -golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= -gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/misc/misc.go b/misc/misc.go index 4f0778b..b3c6652 100644 --- a/misc/misc.go +++ b/misc/misc.go @@ -1,12 +1,15 @@ package misc import ( + "encoding/json" "fmt" "os" + "strconv" "strings" "text/tabwriter" "github.com/akquinet/pdnsgrep/pdns" + "github.com/fatih/color" "github.com/spf13/viper" ) @@ -18,7 +21,18 @@ const ( var headers = []string{"Zone", "Name", "Type", "Content", "TTL", "Object Type"} -// Helper function to format record as string +// Color settings +var ( + headerColor = color.New(color.FgHiWhite, color.Bold) + zoneColor = color.New(color.FgCyan) + nameColor = color.New(color.FgGreen) + typeColor = color.New(color.FgYellow) + contentColor = color.New(color.FgWhite) + ttlColor = color.New(color.FgMagenta) + objectColor = color.New(color.FgBlue) +) + +// Helper function to format record as string (for non-colored output) func formatRecord(record pdns.PDNSSearchResponseItem, delimiter string) string { return fmt.Sprintf("%s%s%s%s%s%s%s%s%d%s%s", record.Zone, delimiter, record.Name, delimiter, record.Type, delimiter, record.Content, delimiter, record.Ttl, delimiter, record.ObjectType) } @@ -39,18 +53,94 @@ func OutputToStdout(records []pdns.PDNSSearchResponseItem) { } func OutputToTable(records []pdns.PDNSSearchResponseItem) { - writer := tabwriter.NewWriter(os.Stdout, 1, 1, 1, ' ', 0) - defer writer.Flush() + // Check if colors should be disabled + if viper.GetBool("no-color") { + // Use the original non-colored output with standard tabwriter + writer := tabwriter.NewWriter(os.Stdout, 0, 8, 1, ' ', 0) + defer writer.Flush() + + if !viper.GetBool("no-header") { + fmt.Fprintln(writer, strings.Join(headers, TabDelimiter)) + } + + for _, r := range records { + fmt.Fprintln(writer, formatRecord(r, TabDelimiter)) + } + return + } + + // For colored output, we'll use a different approach to ensure alignment + // First, calculate the width needed for each column + zoneWidth := len(headers[0]) + nameWidth := len(headers[1]) + typeWidth := len(headers[2]) + contentWidth := len(headers[3]) + ttlWidth := len(headers[4]) + + for _, r := range records { + if len(r.Zone) > zoneWidth { + zoneWidth = len(r.Zone) + } + if len(r.Name) > nameWidth { + nameWidth = len(r.Name) + } + if len(r.Type) > typeWidth { + typeWidth = len(r.Type) + } + if len(r.Content) > contentWidth { + contentWidth = len(r.Content) + } + ttlStr := strconv.Itoa(r.Ttl) + if len(ttlStr) > ttlWidth { + ttlWidth = len(ttlStr) + } + } + + // Add some padding + zoneWidth += 2 + nameWidth += 2 + typeWidth += 2 + contentWidth += 2 + ttlWidth += 2 + // Print headers if !viper.GetBool("no-header") { - fmt.Fprintln(writer, strings.Join(headers, TabDelimiter)) + fmt.Printf("%s%s%s%s%s%s\n", + headerColor.Sprintf("%-*s", zoneWidth, headers[0]), + headerColor.Sprintf("%-*s", nameWidth, headers[1]), + headerColor.Sprintf("%-*s", typeWidth, headers[2]), + headerColor.Sprintf("%-*s", contentWidth, headers[3]), + headerColor.Sprintf("%-*s", ttlWidth, headers[4]), + headerColor.Sprint(headers[5])) } + // Print records with fixed width columns for _, r := range records { - fmt.Fprintln(writer, formatRecord(r, TabDelimiter)) + fmt.Printf("%s%s%s%s%s%s\n", + zoneColor.Sprintf("%-*s", zoneWidth, r.Zone), + nameColor.Sprintf("%-*s", nameWidth, r.Name), + typeColor.Sprintf("%-*s", typeWidth, r.Type), + contentColor.Sprintf("%-*s", contentWidth, r.Content), + ttlColor.Sprintf("%-*d", ttlWidth, r.Ttl), + objectColor.Sprint(r.ObjectType)) } } func OutputToCSV(records []pdns.PDNSSearchResponseItem, delimiter string) { fmt.Print(generateOutput(records, delimiter)) } + +// OutputToJSON outputs records in JSON format +func OutputToJSON(records []pdns.PDNSSearchResponseItem) { + var output []byte + var err error + + output, err = json.MarshalIndent(records, "", " ") + + if err != nil { + fmt.Fprintf(os.Stderr, "Error marshaling to JSON: %v\n", err) + os.Exit(1) + } + + fmt.Println(string(output)) +} diff --git a/pdns/pdns.go b/pdns/pdns.go index 12d583b..b3f14c5 100644 --- a/pdns/pdns.go +++ b/pdns/pdns.go @@ -6,6 +6,7 @@ import ( "fmt" "net/http" "strings" + "time" log "github.com/sirupsen/logrus" "golang.org/x/sync/errgroup" @@ -21,9 +22,11 @@ type PDNSSearchResponseItem struct { } type PDNSAPI struct { - URL string - APIKey string - client http.Client + URL string + APIKey string + Client http.Client + Timeout time.Duration + UserAgent string } func (p *PDNSAPI) Search(query string, objectType string) ([]PDNSSearchResponseItem, error) { @@ -39,7 +42,7 @@ func (p *PDNSAPI) Search(query string, objectType string) ([]PDNSSearchResponseI return nil, err } - resp, err := p.client.Do(req) + resp, err := p.Client.Do(req) if err != nil { return nil, err } @@ -72,6 +75,10 @@ func (p *PDNSAPI) newRequest(method string, path string, params map[string]any) req.Header.Add("Accept", "application/json") req.Header.Add("Content-Type", "application/json") + if p.UserAgent != "" { + req.Header.Add("User-Agent", p.UserAgent) + } + q := req.URL.Query() for k, v := range params { q.Add(k, fmt.Sprintf("%v", v)) @@ -83,9 +90,13 @@ func (p *PDNSAPI) newRequest(method string, path string, params map[string]any) func NewPDNSAPI(url, apiKey string) *PDNSAPI { return &PDNSAPI{ - URL: url, - APIKey: apiKey, - client: http.Client{}, + URL: url, + APIKey: apiKey, + UserAgent: "pdnsgrep/1.0", + Timeout: 10 * time.Second, + Client: http.Client{ + Timeout: 10 * time.Second, + }, } }