Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 83 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,16 @@ The number of worker routines to use to scan ports in parallel. Default is *1000

Only show output for hosts that are confirmed as up.

### `-o [FORMAT]` `--output [FORMAT]`

Output format for scan results. Available formats:

| Format | Description |
|----------|-------------|
| `normal` | Human-readable text output (default) |
| `json` | Structured JSON output similar to nmap's `-oJ` |
| `xml` | Structured XML output similar to nmap's `-oX` |

### `--version`

Output version information and exit.
Expand Down Expand Up @@ -105,6 +115,79 @@ furious -s connect 192.168.1.1
furious -s device 192.168.1.1/24 -u
```

### Generate structured output

#### JSON Output (similar to nmap -oJ)
```
furious -s connect -o json 192.168.1.1
```

Example JSON output:
```json
{
"version": "development version",
"scan_time": "2023-01-01T12:00:00Z",
"args": "192.168.1.1",
"results": [
{
"host": "192.168.1.1",
"status": "up",
"latency": {
"ms": 2.5,
"ns": 2500000
},
"ports": {
"open": [
{
"number": 22,
"protocol": "tcp",
"service": "ssh",
"state": "open"
},
{
"number": 80,
"protocol": "tcp",
"service": "http",
"state": "open"
}
]
}
}
],
"summary": {
"hosts_up": 1,
"hosts_down": 0,
"hosts_total": 1,
"duration": "1.2s",
"ports_scanned": 6000
}
}
```

#### XML Output (similar to nmap -oX)
```
furious -s connect -o xml 192.168.1.1
```

Example XML output:
```xml
<?xml version="1.0" encoding="UTF-8"?>
<furious_scan version="development version" scan_time="2023-01-01T12:00:00Z" args="192.168.1.1">
<results>
<host host="192.168.1.1" status="up">
<latency ms="2.5" ns="2500000"></latency>
<ports>
<open>
<port number="22" protocol="tcp" service="ssh" state="open"></port>
<port number="80" protocol="tcp" service="http" state="open"></port>
</open>
</ports>
</host>
</results>
<summary hosts_up="1" hosts_down="0" hosts_total="1" duration="1.2s" ports_scanned="6000"></summary>
</furious_scan>
```

## Troubleshooting

### `sudo: furious: command not found`
Expand Down
101 changes: 92 additions & 9 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package cmd

import (
"context"
"encoding/json"
"encoding/xml"
"fmt"
"os"
"os/signal"
Expand All @@ -22,6 +24,7 @@ var portSelection string
var scanType = "stealth"
var hideUnavailableHosts bool
var versionRequested bool
var outputFormat string

func init() {
rootCmd.PersistentFlags().BoolVarP(&hideUnavailableHosts, "up-only", "u", hideUnavailableHosts, "Omit output for hosts which are not up")
Expand All @@ -31,22 +34,29 @@ func init() {
rootCmd.PersistentFlags().IntVarP(&timeoutMS, "timeout-ms", "t", timeoutMS, "Scan timeout in MS")
rootCmd.PersistentFlags().IntVarP(&parallelism, "workers", "w", parallelism, "Parallel routines to scan on")
rootCmd.PersistentFlags().StringVarP(&portSelection, "ports", "p", portSelection, "Port to scan. Comma separated, can sue hyphens e.g. 22,80,443,8080-8090")
rootCmd.PersistentFlags().StringVarP(&outputFormat, "output", "o", outputFormat, "Output format: normal, json, xml")
}

func createScanner(ti *scan.TargetIterator, scanTypeStr string, timeout time.Duration, routines int) (scan.Scanner, error) {
func createScanner(ti *scan.TargetIterator, scanTypeStr string, timeout time.Duration, routines int, outputFormat string) (scan.Scanner, error) {
var scanner scan.Scanner
var err error

switch strings.ToLower(scanTypeStr) {
case "stealth", "syn", "fast":
if os.Geteuid() > 0 {
return nil, fmt.Errorf("Access Denied: You must be a priviliged user to run this type of scan.")
}
return scan.NewSynScanner(ti, timeout, routines), nil
scanner = scan.NewSynScanner(ti, timeout, routines)
case "connect":
return scan.NewConnectScanner(ti, timeout, routines), nil
scanner = scan.NewConnectScanner(ti, timeout, routines)
case "device":
return scan.NewDeviceScanner(ti, timeout), nil
scanner = scan.NewDeviceScanner(ti, timeout)
default:
return nil, fmt.Errorf("Unknown scan type '%s'", scanTypeStr)
}

return nil, fmt.Errorf("Unknown scan type '%s'", scanTypeStr)
scanner.SetOutputFormat(outputFormat)
return scanner, err
}

var rootCmd = &cobra.Command{
Expand Down Expand Up @@ -90,14 +100,28 @@ var rootCmd = &cobra.Command{
}()

startTime := time.Now()
fmt.Printf("\nStarting scan at %s\n\n", startTime.String())

// Validate output format
outputFormat = strings.ToLower(outputFormat)
if outputFormat != "" && outputFormat != "normal" && outputFormat != "json" && outputFormat != "xml" {
fmt.Printf("Error: Invalid output format '%s'. Must be one of: normal, json, xml\n", outputFormat)
os.Exit(1)
}

isStructuredOutput := outputFormat == "json" || outputFormat == "xml"

if !isStructuredOutput {
fmt.Printf("\nStarting scan at %s\n\n", startTime.String())
}

allResults := []scan.Result{}

for _, target := range args {

targetIterator := scan.NewTargetIterator(target)

// creating scanner
scanner, err := createScanner(targetIterator, scanType, time.Millisecond*time.Duration(timeoutMS), parallelism)
scanner, err := createScanner(targetIterator, scanType, time.Millisecond*time.Duration(timeoutMS), parallelism, outputFormat)
if err != nil {
fmt.Println(err)
os.Exit(1)
Expand All @@ -119,13 +143,21 @@ var rootCmd = &cobra.Command{

for _, result := range results {
if !hideUnavailableHosts || result.IsHostUp() {
scanner.OutputResult(result)
if isStructuredOutput {
allResults = append(allResults, result)
} else {
scanner.OutputResult(result)
}
}
}

}

fmt.Printf("Scan complete in %s.\n", time.Since(startTime).String())
if isStructuredOutput {
outputStructuredResults(allResults, outputFormat, startTime, strings.Join(args, " "), ports)
} else {
fmt.Printf("Scan complete in %s.\n", time.Since(startTime).String())
}

},
}
Expand Down Expand Up @@ -179,3 +211,54 @@ func getPorts(selection string) ([]int, error) {
}
return ports, nil
}

func outputStructuredResults(results []scan.Result, format string, startTime time.Time, args string, ports []int) {
// Convert results to structured format
structuredResults := make([]scan.StructuredResult, 0, len(results))
hostsUp := 0

for _, result := range results {
structured := result.ToStructured()
structuredResults = append(structuredResults, structured)
if result.IsHostUp() {
hostsUp++
}
}

// Create scan results wrapper
scanResults := scan.ScanResults{
Version: version.Version,
ScanTime: startTime.Format(time.RFC3339),
Args: args,
Results: structuredResults,
Summary: scan.ScanSummary{
HostsUp: hostsUp,
HostsDown: len(results) - hostsUp,
HostsTotal: len(results),
Duration: time.Since(startTime).String(),
PortsScanned: len(ports),
},
}

var output []byte
var err error

switch format {
case "json":
output, err = json.MarshalIndent(scanResults, "", " ")
case "xml":
output, err = xml.MarshalIndent(scanResults, "", " ")
if err == nil {
// Add XML header
xmlHeader := []byte(xml.Header)
output = append(xmlHeader, output...)
}
}

if err != nil {
fmt.Printf("Error generating %s output: %v\n", format, err)
os.Exit(1)
}

fmt.Println(string(output))
}
Loading