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
31 changes: 31 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,10 @@ 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.

### `-j` `--json`

Output results in JSON format instead of the default human-readable format. This is useful for programmatic processing of scan results.

### `--version`

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

### Output results in JSON format

```
furious -s connect -j 192.168.1.1
```

Example JSON output:
```json
[
{
"host": "192.168.1.1",
"status": "up",
"latency_ms": 2.5,
"open_ports": [
{
"port": 22,
"service": "ssh"
},
{
"port": 80,
"service": "http"
}
]
}
]
```

## Troubleshooting

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

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

func init() {
rootCmd.PersistentFlags().BoolVarP(&hideUnavailableHosts, "up-only", "u", hideUnavailableHosts, "Omit output for hosts which are not up")
Expand All @@ -31,22 +33,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().BoolVarP(&jsonOutput, "json", "j", jsonOutput, "Output results in JSON format")
}

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, jsonOutput bool) (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.SetJSONOutput(jsonOutput)
return scanner, err
}

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

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

if !jsonOutput {
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, jsonOutput)
if err != nil {
fmt.Println(err)
os.Exit(1)
Expand All @@ -119,13 +133,21 @@ var rootCmd = &cobra.Command{

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

}

fmt.Printf("Scan complete in %s.\n", time.Since(startTime).String())
if jsonOutput {
outputJSONResults(allResults)
} else {
fmt.Printf("Scan complete in %s.\n", time.Since(startTime).String())
}

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

func outputJSONResults(results []scan.Result) {
jsonResults := make([]interface{}, 0, len(results))

for _, result := range results {
if jsonData, err := result.ToJSON(); err == nil {
var jsonObj interface{}
if err := json.Unmarshal(jsonData, &jsonObj); err == nil {
jsonResults = append(jsonResults, jsonObj)
}
}
}

if output, err := json.MarshalIndent(jsonResults, "", " "); err == nil {
fmt.Println(string(output))
} else {
fmt.Printf("Error marshaling results to JSON: %v\n", err)
}
}
71 changes: 63 additions & 8 deletions scan/result.go
Original file line number Diff line number Diff line change
@@ -1,20 +1,21 @@
package scan

import (
"encoding/json"
"fmt"
"net"
"time"
)

type Result struct {
Host net.IP
Open []int
Closed []int
Filtered []int
Manufacturer string
MAC string
Latency time.Duration
Name string
Host net.IP `json:"host"`
Open []int `json:"open_ports"`
Closed []int `json:"closed_ports"`
Filtered []int `json:"filtered_ports"`
Manufacturer string `json:"manufacturer,omitempty"`
MAC string `json:"mac,omitempty"`
Latency time.Duration `json:"latency_ns"`
Name string `json:"hostname,omitempty"`
}

func NewResult(host net.IP) Result {
Expand Down Expand Up @@ -70,3 +71,57 @@ func pad(input string, length int) string {
}
return input
}

// JSONResult represents a result in JSON-friendly format
type JSONResult struct {
Host string `json:"host"`
Status string `json:"status"`
Open []PortInfo `json:"open_ports"`
Closed []int `json:"closed_ports,omitempty"`
Filtered []int `json:"filtered_ports,omitempty"`
Manufacturer string `json:"manufacturer,omitempty"`
MAC string `json:"mac,omitempty"`
LatencyMs float64 `json:"latency_ms"`
Hostname string `json:"hostname,omitempty"`
}

// PortInfo represents port information for JSON output
type PortInfo struct {
Port int `json:"port"`
Service string `json:"service"`
}

// ToJSON converts Result to JSON format
func (r Result) ToJSON() ([]byte, error) {
status := "down"
if r.IsHostUp() {
status = "up"
}

openPorts := make([]PortInfo, len(r.Open))
for i, port := range r.Open {
openPorts[i] = PortInfo{
Port: port,
Service: DescribePort(port),
}
}

latencyMs := float64(0)
if r.Latency > 0 {
latencyMs = float64(r.Latency.Nanoseconds()) / 1000000.0
}

jsonResult := JSONResult{
Host: r.Host.String(),
Status: status,
Open: openPorts,
Closed: r.Closed,
Filtered: r.Filtered,
Manufacturer: r.Manufacturer,
MAC: r.MAC,
LatencyMs: latencyMs,
Hostname: r.Name,
}

return json.Marshal(jsonResult)
}
76 changes: 76 additions & 0 deletions scan/result_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package scan

import (
"encoding/json"
"net"
"testing"
"time"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestResultToJSON(t *testing.T) {
// Create a test result
result := Result{
Host: net.ParseIP("192.168.1.1"),
Open: []int{22, 80, 443},
Closed: []int{21, 23},
Filtered: []int{8080},
Latency: time.Millisecond * 50,
MAC: "00:11:22:33:44:55",
Name: "test-host",
}

// Convert to JSON
jsonData, err := result.ToJSON()
require.NoError(t, err)

// Parse the JSON to verify structure
var jsonResult JSONResult
err = json.Unmarshal(jsonData, &jsonResult)
require.NoError(t, err)

// Verify the JSON structure
assert.Equal(t, "192.168.1.1", jsonResult.Host)
assert.Equal(t, "up", jsonResult.Status)
assert.Equal(t, float64(50), jsonResult.LatencyMs)
assert.Equal(t, "00:11:22:33:44:55", jsonResult.MAC)
assert.Equal(t, "test-host", jsonResult.Hostname)

// Verify open ports
require.Len(t, jsonResult.Open, 3)
assert.Equal(t, 22, jsonResult.Open[0].Port)
assert.Equal(t, "ssh", jsonResult.Open[0].Service)
assert.Equal(t, 80, jsonResult.Open[1].Port)
assert.Equal(t, "http", jsonResult.Open[1].Service)
assert.Equal(t, 443, jsonResult.Open[2].Port)
assert.Equal(t, "https", jsonResult.Open[2].Service)

// Verify closed and filtered ports
assert.Equal(t, []int{21, 23}, jsonResult.Closed)
assert.Equal(t, []int{8080}, jsonResult.Filtered)
}

func TestResultToJSONDownHost(t *testing.T) {
// Create a test result for a down host
result := Result{
Host: net.ParseIP("192.168.1.100"),
Latency: -1, // Indicates host is down
}

// Convert to JSON
jsonData, err := result.ToJSON()
require.NoError(t, err)

// Parse the JSON to verify structure
var jsonResult JSONResult
err = json.Unmarshal(jsonData, &jsonResult)
require.NoError(t, err)

// Verify the JSON structure for down host
assert.Equal(t, "192.168.1.100", jsonResult.Host)
assert.Equal(t, "down", jsonResult.Status)
assert.Equal(t, float64(0), jsonResult.LatencyMs)
assert.Empty(t, jsonResult.Open)
}
15 changes: 14 additions & 1 deletion scan/scan-connect.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ type ConnectScanner struct {
maxRoutines int
jobChan chan portJob
ti *TargetIterator
jsonOutput bool
}

func NewConnectScanner(ti *TargetIterator, timeout time.Duration, paralellism int) *ConnectScanner {
Expand Down Expand Up @@ -195,5 +196,17 @@ func (s *ConnectScanner) scanPort(target net.IP, port int) (PortState, error) {
}

func (s *ConnectScanner) OutputResult(result Result) {
fmt.Println(result.String())
if s.jsonOutput {
if jsonData, err := result.ToJSON(); err == nil {
fmt.Println(string(jsonData))
} else {
fmt.Printf("Error marshaling result to JSON: %v\n", err)
}
} else {
fmt.Println(result.String())
}
}

func (s *ConnectScanner) SetJSONOutput(enabled bool) {
s.jsonOutput = enabled
}
Loading