Skip to content

Commit

Permalink
#77: Possibility to read xml content from stdin (#92)
Browse files Browse the repository at this point in the history
* #77: Possibility to read xml content from stdin

* Remove commented out code

* Remove commented out code
  • Loading branch information
vdjagilev authored Apr 28, 2022
1 parent c92e5ee commit 4520da0
Show file tree
Hide file tree
Showing 8 changed files with 185 additions and 30 deletions.
28 changes: 21 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
```

Expand All @@ -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
```

Expand All @@ -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'
```

Expand Down
21 changes: 13 additions & 8 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down Expand Up @@ -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
}
14 changes: 9 additions & 5 deletions cmd/root_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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
Expand Down
12 changes: 6 additions & 6 deletions formatter/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
35 changes: 35 additions & 0 deletions formatter/file.go
Original file line number Diff line number Diff line change
@@ -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
}
71 changes: 71 additions & 0 deletions formatter/file_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
})
}
}
16 changes: 15 additions & 1 deletion formatter/workflow.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 == "" {
Expand All @@ -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 {
Expand All @@ -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
}
Expand Down
18 changes: 15 additions & 3 deletions formatter/workflow_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{},
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down

0 comments on commit 4520da0

Please # to comment.