From 4520da0a91420a3c796591a06ddd3d1f289b0a35 Mon Sep 17 00:00:00 2001 From: Vsevolod Djagilev <2762286+vdjagilev@users.noreply.github.com> Date: Thu, 28 Apr 2022 16:06:41 +0300 Subject: [PATCH] #77: Possibility to read xml content from stdin (#92) * #77: Possibility to read xml content from stdin * Remove commented out code * Remove commented out code --- README.md | 28 +++++++++++---- cmd/root.go | 21 ++++++----- cmd/root_test.go | 14 +++++--- formatter/config.go | 12 +++---- formatter/file.go | 35 +++++++++++++++++++ formatter/file_test.go | 71 ++++++++++++++++++++++++++++++++++++++ formatter/workflow.go | 16 ++++++++- formatter/workflow_test.go | 18 ++++++++-- 8 files changed, 185 insertions(+), 30 deletions(-) create mode 100644 formatter/file_test.go diff --git a/README.md b/README.md index 5c27b2b..b5e09a8 100644 --- a/README.md +++ b/README.md @@ -25,31 +25,45 @@ A tool that allows you to convert NMAP XML output to html/csv/json/markdown. ## Usage -``` +```bash nmap-formatter [html|csv|md|json] [path-to-nmap.xml] [flags] ``` -Convert XML output to nicer HTML +Or alternatively you can read file from `stdin` and parse it +```bash +cat some.xml | nmap-formatter json ``` + +Convert XML output to nicer HTML + +```bash nmap-formatter html [path-to-nmap.xml] > some-file.html ``` or Markdown -``` +```bash nmap-formatter md [path-to-nmap.xml] > some-markdown.md ``` or JSON -``` +```bash nmap-formatter json [path-to-nmap.xml] +# This approach is also possible +cat nmap.xml | nmap-formatter json ``` -it can be also combined with a `jq` tool, for example, list all the found ports and count them: +It can be also combined with a `jq` tool +```bash +cat nmap.xml | nmap-formatter json | jq ``` + +List all the found ports and count them: + +```bash nmap-formatter json [nmap.xml] | jq -r '.Host[]?.Ports?.Port[]?.PortID' | sort | uniq -c ``` @@ -61,7 +75,7 @@ nmap-formatter json [nmap.xml] | jq -r '.Host[]?.Ports?.Port[]?.PortID' | sort | another example where only those hosts are selected, which have port where some http service is running: -``` +```bash nmap-formatter json [nmap.xml] | jq '.Host[]? | . as $host | .Ports?.Port[]? | select(.Service.Name== "http") | $host.HostAddress.Address' | uniq -c ``` @@ -81,7 +95,7 @@ nmap-formatter json [nmap.xml] | jq '.Host[]?.Ports?.Port[]? | select(.State.Sta Display host IP addresses that have filtered ports: -``` +```bash nmap-formatter json [nmap.xml] | jq '.Host[]? | . as $host | .Ports?.Port[]? | select(.State.State == "filtered") | .PortID | $host.HostAddress.Address' ``` diff --git a/cmd/root.go b/cmd/root.go index 50fa529..5d78919 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -107,11 +107,15 @@ func arguments(cmd *cobra.Command, args []string) error { if len(args) < 1 { return errors.New("requires output format argument") } - if len(args) < 2 { - return errors.New("requires an xml file argument") - } + config.OutputFormat = formatter.OutputFormat(args[0]) - config.InputFile = formatter.InputFile(args[1]) + config.InputFileConfig = formatter.InputFileConfig{} + + if len(args) > 1 { + config.InputFileConfig.Path = args[1] + } else { + config.InputFileConfig.IsStdin = true + } return nil } @@ -153,10 +157,11 @@ func validate(config formatter.Config) error { } // Checking if xml file is readable - f, err := os.Open(string(config.InputFile)) - if err != nil { - return fmt.Errorf("could not open XML file: %v", err) + if !config.InputFileConfig.IsStdin { + err := config.InputFileConfig.ExistsOpen() + if err != nil { + return fmt.Errorf("could not open XML file: %v", err) + } } - defer f.Close() return nil } diff --git a/cmd/root_test.go b/cmd/root_test.go index 28096c5..7b73bcd 100644 --- a/cmd/root_test.go +++ b/cmd/root_test.go @@ -68,7 +68,9 @@ func Test_validate(t *testing.T) { args: args{ config: formatter.Config{ OutputFormat: formatter.CSVOutput, - InputFile: formatter.InputFile(path.Join(os.TempDir(), "formatter_cmd_valid_2")), + InputFileConfig: formatter.InputFileConfig{ + Path: path.Join(os.TempDir(), "formatter_cmd_valid_2"), + }, }, }, wantErr: false, @@ -115,11 +117,11 @@ func Test_arguments(t *testing.T) { wantErr: true, }, { - name: "No Output format argument provided", + name: "No Output format argument provided (reading from stdin)", args: args{ - args: []string{"file.xml"}, + args: []string{"html"}, }, - wantErr: true, + wantErr: false, }, { name: "Version argument provided", @@ -163,7 +165,9 @@ func Test_run(t *testing.T) { } workflow = testWorkflow config = testConfig - config.InputFile = formatter.InputFile(file) + config.InputFileConfig = formatter.InputFileConfig{ + Path: file, + } } after := func(file string, t *testing.T) { var err error diff --git a/formatter/config.go b/formatter/config.go index 32f6305..9234639 100644 --- a/formatter/config.go +++ b/formatter/config.go @@ -8,10 +8,10 @@ import ( // where output will be delivered, desired output format, input file path, output file path // and different output options type Config struct { - Writer io.Writer - OutputFormat OutputFormat - InputFile InputFile - OutputFile OutputFile - OutputOptions OutputOptions - ShowVersion bool + Writer io.Writer + OutputFormat OutputFormat + InputFileConfig InputFileConfig + OutputFile OutputFile + OutputOptions OutputOptions + ShowVersion bool } diff --git a/formatter/file.go b/formatter/file.go index 2146c69..e807892 100644 --- a/formatter/file.go +++ b/formatter/file.go @@ -1,7 +1,42 @@ package formatter +import ( + "bufio" + "errors" + "io" + "os" +) + // OutputFile describes output file name (full path) type OutputFile string // InputFile describes input file (nmap XML full path) type InputFile string + +type InputFileConfig struct { + Path string + IsStdin bool + Source io.Reader +} + +// ReadContents reads content from stdin or provided file-path +func (i *InputFileConfig) ReadContents() ([]byte, error) { + var err error + var content []byte + if i.Source == nil { + return nil, errors.New("no reading source is defined") + } + scanner := bufio.NewScanner(i.Source) + for scanner.Scan() { + content = append(content, scanner.Bytes()...) + } + err = scanner.Err() + return content, err +} + +// ExistsOpen tries to open a file for reading, returning an error if it fails +func (i *InputFileConfig) ExistsOpen() error { + f, err := os.Open(i.Path) + f.Close() + return err +} diff --git a/formatter/file_test.go b/formatter/file_test.go new file mode 100644 index 0000000..03dac1c --- /dev/null +++ b/formatter/file_test.go @@ -0,0 +1,71 @@ +package formatter + +import ( + "os" + "path" + "testing" +) + +func TestInputFileConfig_ExistsOpen(t *testing.T) { + type fields struct { + Path string + IsStdin bool + } + beforeFunc := func(path string, t *testing.T) { + f, err := os.Create(path) + if err != nil { + t.Errorf("error creating temporary file: %s", err) + } + defer f.Close() + } + afterFunc := func(name string) { + os.Remove(name) + } + tests := []struct { + name string + fields fields + wantErr bool + file string + runBefore bool + runAfter bool + before func(path string, t *testing.T) + after func(path string) + }{ + { + name: "File does not exist", + fields: fields{ + Path: "", + }, + wantErr: true, + }, + { + name: "File exists", + fields: fields{ + Path: path.Join(os.TempDir(), "inputfile_config_test_exists_2.txt"), + }, + wantErr: false, + file: path.Join(os.TempDir(), "inputfile_config_test_exists_2.txt"), + before: beforeFunc, + after: afterFunc, + runBefore: true, + runAfter: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.runBefore { + tt.before(tt.file, t) + } + if tt.runAfter { + defer tt.after(tt.file) + } + i := &InputFileConfig{ + Path: tt.fields.Path, + IsStdin: tt.fields.IsStdin, + } + if err := i.ExistsOpen(); (err != nil) != tt.wantErr { + t.Errorf("InputFileConfig.ExistsOpen() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/formatter/workflow.go b/formatter/workflow.go index a8debb3..1676b9f 100644 --- a/formatter/workflow.go +++ b/formatter/workflow.go @@ -25,6 +25,9 @@ func (w *MainWorkflow) SetConfig(c *Config) { // Execute is the core of the application which executes required steps // one-by-one to achieve formatting from input -> output. func (w *MainWorkflow) Execute() (err error) { + var inputFile *os.File + // This one is read in `parse()` function, we can close it here + defer inputFile.Close() // If no output file has been provided all content // goes to the STDOUT if w.Config.OutputFile == "" { @@ -40,6 +43,17 @@ func (w *MainWorkflow) Execute() (err error) { w.Config.Writer = f } + // Set InputFileConfig source to stdin or specific file + if w.Config.InputFileConfig.IsStdin { + inputFile = os.Stdin + } else { + inputFile, err = os.Open(w.Config.InputFileConfig.Path) + if err != nil { + return + } + } + w.Config.InputFileConfig.Source = inputFile + // Reading & parsing the input file NMAPRun, err := w.parse() if err != nil { @@ -66,7 +80,7 @@ func (w *MainWorkflow) Execute() (err error) { // parse reads & unmarshalles the input file into NMAPRun struct func (w *MainWorkflow) parse() (run NMAPRun, err error) { - input, err := os.ReadFile(string(w.Config.InputFile)) + input, err := w.Config.InputFileConfig.ReadContents() if err != nil { return } diff --git a/formatter/workflow_test.go b/formatter/workflow_test.go index d238b49..3c260f9 100644 --- a/formatter/workflow_test.go +++ b/formatter/workflow_test.go @@ -20,7 +20,9 @@ func TestMainWorkflow_parse(t *testing.T) { name: "Wrong path (file does not exists)", w: &MainWorkflow{ Config: &Config{ - InputFile: InputFile(""), + InputFileConfig: InputFileConfig{ + Path: "", + }, }, }, wantNMAPRun: NMAPRun{}, @@ -79,7 +81,15 @@ func TestMainWorkflow_parse(t *testing.T) { } // deferring file removal after the test defer os.Remove(name) - tt.w.Config.InputFile = InputFile(name) + f, err := os.Open(name) + if err != nil { + t.Errorf("could not read source file: %v", err) + } + defer f.Close() + tt.w.Config.InputFileConfig = InputFileConfig{ + Path: name, + Source: f, + } } gotNMAPRun, err := tt.w.parse() if (err != nil) != tt.wantErr { @@ -173,7 +183,9 @@ func TestMainWorkflow_Execute(t *testing.T) { } defer os.Remove(name) defer os.Remove(name + "_output") - tt.w.Config.InputFile = InputFile(name) + tt.w.Config.InputFileConfig = InputFileConfig{ + Path: name, + } tt.w.Config.OutputFile = OutputFile(name + "_output") } if err := tt.w.Execute(); (err != nil) != tt.wantErr {