diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 0000000..921d35d --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,26 @@ +name: Lint + +on: + push: + branches: + - main + pull_request: + branches: + - main + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - name: Set up Go + uses: actions/setup-go@v4 + with: + go-version-file: 'go.mod' + + - name: Lint + uses: golangci/golangci-lint-action@v3 + with: + version: v1.55.2 + skip-pkg-cache: true diff --git a/.github/workflows/test-coverage.yml b/.github/workflows/test-coverage.yml index 4b70dd1..6f05268 100644 --- a/.github/workflows/test-coverage.yml +++ b/.github/workflows/test-coverage.yml @@ -1,4 +1,4 @@ -name: Test +name: Test and Update Coverage on: pull_request: @@ -6,9 +6,8 @@ on: - main jobs: - test: + test_coverage: runs-on: ubuntu-latest - name: Test and update coverage badge steps: - name: Checkout uses: actions/checkout@v4 @@ -25,12 +24,6 @@ jobs: - name: Install dependencies run: sudo apt update && sudo apt install -y make libpcap-dev - - name: Lint - uses: golangci/golangci-lint-action@v3 - with: - version: v1.55.2 - skip-pkg-cache: true - - name: Run Test run: | make test diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index f06c54f..8eaf227 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -8,8 +8,7 @@ on: - '!v*' jobs: - build: - + build_and_test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 @@ -25,11 +24,5 @@ jobs: - name: Build run: make - - name: Lint - uses: golangci/golangci-lint-action@v3 - with: - version: v1.55.2 - skip-pkg-cache: true - - name: Test run: make test diff --git a/Makefile b/Makefile index 953569d..bb01093 100644 --- a/Makefile +++ b/Makefile @@ -99,6 +99,7 @@ test: -v \ -coverprofile $(coverage_profile) \ -covermode=atomic \ + -race \ ./... .PHONY: print-coverage diff --git a/README.md b/README.md index 1f7873b..bfa03d5 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ # go-lanscan -![Coverage](https://img.shields.io/badge/Coverage-92.5%25-brightgreen) +![Coverage](https://img.shields.io/badge/Coverage-92.2%25-brightgreen) A network cli and golang package that allows you to perform arp and syn scanning on a local area network. diff --git a/go.mod b/go.mod index 537dcda..ff6d8c5 100644 --- a/go.mod +++ b/go.mod @@ -8,6 +8,7 @@ require ( github.com/jedib0t/go-pretty v4.3.0+incompatible github.com/klauspost/oui v0.0.0-20150225163751-35b4deb627f8 github.com/rs/zerolog v1.31.0 + github.com/schollz/progressbar/v3 v3.14.1 github.com/spf13/cobra v1.8.0 github.com/stretchr/testify v1.8.4 github.com/thediveo/netdb v1.0.3 @@ -23,6 +24,7 @@ require ( github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-runewidth v0.0.15 // indirect + github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/oklog/ulid v1.3.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect @@ -32,7 +34,8 @@ require ( github.com/stretchr/objx v0.5.1 // indirect go.mongodb.org/mongo-driver v1.13.1 // indirect golang.org/x/net v0.19.0 // indirect - golang.org/x/sys v0.15.0 // indirect + golang.org/x/sys v0.16.0 // indirect + golang.org/x/term v0.16.0 // indirect golang.org/x/tools v0.14.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index e676d5a..5d68af7 100644 --- a/go.sum +++ b/go.sum @@ -31,6 +31,7 @@ github.com/jackpal/gateway v1.0.13 h1:fJccMvawxx0k7S1q7Fy/SXFE0R3hMXkMuw8y9SofWA github.com/jackpal/gateway v1.0.13/go.mod h1:6c8LjW+FVESFmwxaXySkt7fU98Yv806ADS3OY6Cvh2U= github.com/jedib0t/go-pretty v4.3.0+incompatible h1:CGs8AVhEKg/n9YbUenWmNStRW2PHJzaeDodcfvRAbIo= github.com/jedib0t/go-pretty v4.3.0+incompatible/go.mod h1:XemHduiw8R651AF9Pt4FwCTKeG3oo7hrHJAoznj9nag= +github.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213/go.mod h1:vNUNkEQ1e29fT/6vq2aBdFsgNPmy8qMdSay1npru+Sw= github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= github.com/klauspost/oui v0.0.0-20150225163751-35b4deb627f8 h1:8vTSNy6M0xiuAOmKh271gD8sr6mM+5RzXAiqIUL0KmE= github.com/klauspost/oui v0.0.0-20150225163751-35b4deb627f8/go.mod h1:iaF36Fc2UmrXJ7AGL+fEZU9WWuZiB+4dp9tQtADeZ6A= @@ -46,6 +47,8 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db h1:62I3jR2EmQ4l5rM/4FEfDWcRD+abF5XlKShorW5LRoQ= +github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db/go.mod h1:l0dey0ia/Uv7NcFFVbCLtqEBQbrT4OCwCSKTEv6enCw= 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/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= @@ -69,6 +72,8 @@ github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= github.com/rs/zerolog v1.31.0 h1:FcTR3NnLWW+NnTwwhFWiJSZr4ECLpqCm6QsEnyvbV4A= github.com/rs/zerolog v1.31.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/schollz/progressbar/v3 v3.14.1 h1:VD+MJPCr4s3wdhTc7OEJ/Z3dAeBzJ7yKH/P4lC5yRTI= +github.com/schollz/progressbar/v3 v3.14.1/go.mod h1:Zc9xXneTzWXF81TGoqL71u0sBPjULtEHYtj/WVgVy8E= github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0= github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= @@ -78,6 +83,7 @@ github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSS github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.1 h1:4VhoImhV/Bm0ToFkXFi8hXNXwpDRZ/ynw3amt82mzq0= github.com/stretchr/objx v0.5.1/go.mod h1:/iHQpkQwBD6DLUmQ4pE+s1TXdob1mORJ4/UFdrifcy0= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.6.1/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= @@ -121,10 +127,14 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBc 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.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= -golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU= +golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.14.0/go.mod h1:TySc+nGkYR6qt8km8wUhuFRTVSMIX3XPR58y2lC8vww= +golang.org/x/term v0.16.0 h1:m+B6fahuftsE9qjo0VWp2FW0mB3MTJvR0BaMQrq0pmE= +golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= diff --git a/internal/core/core.go b/internal/core/core.go index 756edfa..9bda482 100644 --- a/internal/core/core.go +++ b/internal/core/core.go @@ -12,12 +12,12 @@ import ( "sync" "time" - "github.com/jedib0t/go-pretty/progress" "github.com/jedib0t/go-pretty/table" "github.com/robgonnella/go-lanscan/internal/logger" "github.com/robgonnella/go-lanscan/internal/util" "github.com/robgonnella/go-lanscan/pkg/scanner" "github.com/rs/zerolog" + "github.com/schollz/progressbar/v3" ) // DeviceResult represents a discovered network device @@ -70,9 +70,8 @@ type Core struct { outFile string portLen int results *Results - pw progress.Writer - arpTracker *progress.Tracker - synTracker *progress.Tracker + arpProgress *progressbar.ProgressBar + synProgress *progressbar.ProgressBar requestNotifier chan *scanner.Request errorChan chan error scanner scanner.Scanner @@ -99,15 +98,10 @@ func (c *Core) Initialize( printJSON bool, outFile string, ) { - pw := progressWriter() - results := &Results{ Devices: []*DeviceResult{}, } - arpTracker := &progress.Tracker{Message: "starting arp scan"} - arpTracker.Total = int64(targetLen) - if noProgress { logger.SetGlobalLevel(zerolog.Disabled) } else { @@ -118,9 +112,8 @@ func (c *Core) Initialize( c.results = results c.errorChan = make(chan error) c.portLen = portLen - c.pw = pw - c.arpTracker = arpTracker - c.synTracker = &progress.Tracker{Message: "starting syn scan"} + c.arpProgress = newProgressBar(targetLen, "performing arp scan") + c.synProgress = newProgressBar(1, "performing syn scan") c.noProgress = noProgress c.arpOnly = arpOnly c.printJSON = printJSON @@ -132,9 +125,7 @@ func (c *Core) Run() error { start := time.Now() if !c.noProgress { - c.pw.AppendTracker(c.arpTracker) go c.monitorRequestNotifications() - go c.pw.Render() } // run in go routine so we can process in results in parallel @@ -243,10 +234,8 @@ func (c *Core) processArpDone() { c.printArpResults() if !c.noProgress && !c.arpOnly && len(c.results.Devices) > 0 { - c.synTracker.Total = int64( - len(c.results.Devices) * c.portLen, - ) - c.pw.AppendTracker(c.synTracker) + size := len(c.results.Devices) * c.portLen + c.synProgress.ChangeMax(size) } if !c.arpOnly && len(c.results.Devices) == 0 { @@ -361,21 +350,21 @@ func (c *Core) monitorRequestNotifications() { for r := range c.requestNotifier { switch r.Type { case scanner.ArpRequest: - c.arpTracker.Increment(1) + // nolint:errcheck + c.arpProgress.Add(1) message := fmt.Sprintf("arp - scanning %s", r.IP) - if c.arpTracker.IsDone() { - message = "arp - scan complete" - // delay to print line after message is updated - time.AfterFunc(time.Millisecond*100, func() { - c.log.Info().Msg("compiling arp results...") - }) - } + c.arpProgress.Describe("\033[36m" + message + "\033[0m") - c.arpTracker.Message = message + if c.arpProgress.IsFinished() { + // nolint:errcheck + c.arpProgress.Clear() + c.log.Info().Msg("compiling arp results...") + } case scanner.SynRequest: - c.synTracker.Increment(1) + // nolint:errcheck + c.synProgress.Add(1) message := fmt.Sprintf( "syn - scanning port %d on %s", @@ -383,33 +372,29 @@ func (c *Core) monitorRequestNotifications() { r.IP, ) - if c.synTracker.IsDone() { - message = "syn - scan complete" - // delay to print line after message is updated - time.AfterFunc(time.Millisecond*100, func() { - c.log.Info().Msg("compiling syn results...") - }) - } + c.synProgress.Describe("\033[36m" + message + "\033[0m") - c.synTracker.Message = message + if c.synProgress.IsFinished() { + // nolint:errcheck + c.synProgress.Clear() + c.log.Info().Msg("compiling syn results...") + } } } } -// helpers -func progressWriter() progress.Writer { - pw := progress.NewWriter() - pw.SetOutputWriter(os.Stdout) - pw.SetAutoStop(false) - pw.SetTrackerLength(25) - pw.SetMessageWidth(47) - pw.SetNumTrackersExpected(1) - pw.SetSortBy(progress.SortByPercentDsc) - pw.SetStyle(progress.StyleDefault) - pw.SetTrackerPosition(progress.PositionRight) - pw.SetUpdateFrequency(time.Millisecond * 100) - pw.Style().Colors = progress.StyleColorsExample - pw.Style().Options.PercentFormat = "%4.3f%%" - - return pw +func newProgressBar(size int, msg string) *progressbar.ProgressBar { + return progressbar.NewOptions(size, + progressbar.OptionUseANSICodes(true), + progressbar.OptionEnableColorCodes(true), + progressbar.OptionShowBytes(true), + progressbar.OptionSetWidth(25), + progressbar.OptionSetDescription("\033[36m"+msg+"\033[0m"), + progressbar.OptionSetTheme(progressbar.Theme{ + Saucer: "[green]=[reset]", + SaucerHead: "[green]>[reset]", + SaucerPadding: " ", + BarStart: "[", + BarEnd: "]", + })) } diff --git a/pkg/scanner/arpscan.go b/pkg/scanner/arpscan.go index 189ce3a..653125a 100644 --- a/pkg/scanner/arpscan.go +++ b/pkg/scanner/arpscan.go @@ -4,7 +4,6 @@ package scanner import ( "bytes" - "context" "net" "sync" "time" @@ -21,8 +20,7 @@ import ( // ArpScanner implements the Scanner interface for ARP scanning type ArpScanner struct { - ctx context.Context - cancel context.CancelFunc + cancel chan struct{} targets []string networkInfo network.Network cap PacketCapture @@ -45,11 +43,7 @@ func NewArpScanner( networkInfo network.Network, options ...Option, ) *ArpScanner { - ctx, cancel := context.WithCancel(context.Background()) - scanner := &ArpScanner{ - ctx: ctx, - cancel: cancel, targets: targets, cap: &defaultPacketCapture{}, networkInfo: networkInfo, @@ -112,6 +106,7 @@ func (s *ArpScanner) Scan() error { s.scanningMux.Unlock() s.handle = handle + s.cancel = make(chan struct{}) go s.readPackets() @@ -143,7 +138,9 @@ func (s *ArpScanner) Scan() error { // Stop stops the scanner func (s *ArpScanner) Stop() { - s.cancel() + if s.cancel != nil { + close(s.cancel) + } if s.handle != nil { s.handle.Close() @@ -181,12 +178,12 @@ func (s *ArpScanner) SetPacketCapture(cap PacketCapture) { } func (s *ArpScanner) readPackets() { - stopChan := make(chan struct{}) + done := make(chan struct{}) go func() { for { select { - case <-stopChan: + case <-done: return default: var eth layers.Ethernet @@ -222,15 +219,13 @@ func (s *ArpScanner) readPackets() { }() defer func() { - go func() { - stopChan <- struct{}{} - }() + go close(done) s.reset() }() for { select { - case <-s.ctx.Done(): + case <-s.cancel: return default: s.packetSentAtMux.RLock() @@ -350,10 +345,4 @@ func (s *ArpScanner) reset() { s.packetSentAtMux.Lock() s.lastPacketSentAt = time.Time{} s.packetSentAtMux.Unlock() - - if s.ctx.Err() != nil { - ctx, cancel := context.WithCancel(context.Background()) - s.ctx = ctx - s.cancel = cancel - } } diff --git a/pkg/scanner/fullscan.go b/pkg/scanner/fullscan.go index 8e52abc..71a866a 100644 --- a/pkg/scanner/fullscan.go +++ b/pkg/scanner/fullscan.go @@ -4,7 +4,6 @@ package scanner import ( "bytes" - "context" "fmt" "slices" "sync" @@ -18,8 +17,7 @@ import ( // FullScanner implements Scanner interface to perform both ARP and SYN scanning type FullScanner struct { - ctx context.Context - cancel context.CancelFunc + cancel chan struct{} targets []string ports []string listenPort uint16 @@ -43,8 +41,6 @@ func NewFullScanner( listenPort uint16, options ...Option, ) *FullScanner { - ctx, cancel := context.WithCancel(context.Background()) - arpScanner := NewArpScanner( targets, netInfo, @@ -58,8 +54,6 @@ func NewFullScanner( ) scanner := &FullScanner{ - ctx: ctx, - cancel: cancel, netInfo: netInfo, targets: targets, listenPort: listenPort, @@ -101,6 +95,8 @@ func (s *FullScanner) Scan() error { s.scanning = true s.scanningMux.Unlock() + s.cancel = make(chan struct{}) + defer s.reset() go func() { @@ -111,7 +107,7 @@ func (s *FullScanner) Scan() error { for { select { - case <-s.ctx.Done(): + case <-s.cancel: return nil case r := <-s.arpScanner.Results(): switch r.Type { @@ -142,7 +138,9 @@ func (s *FullScanner) Scan() error { // Stop stops the scanner func (s *FullScanner) Stop() { - s.cancel() + if s.cancel != nil { + close(s.cancel) + } if s.arpScanner != nil { s.arpScanner.Stop() @@ -238,10 +236,4 @@ func (s *FullScanner) reset() { s.scanningMux.Lock() s.scanning = false s.scanningMux.Unlock() - - if s.ctx.Err() != nil { - ctx, cancel := context.WithCancel(context.Background()) - s.ctx = ctx - s.cancel = cancel - } } diff --git a/pkg/scanner/synscan.go b/pkg/scanner/synscan.go index 928f065..077ccb4 100644 --- a/pkg/scanner/synscan.go +++ b/pkg/scanner/synscan.go @@ -3,7 +3,6 @@ package scanner import ( - "context" "fmt" "sync" "time" @@ -27,8 +26,7 @@ type SynPacket struct { // SynScanner implements the Scanner interface for SYN scanning type SynScanner struct { - ctx context.Context - cancel context.CancelFunc + cancel chan struct{} networkInfo network.Network targets []*ArpScanResult ports []string @@ -55,11 +53,7 @@ func NewSynScanner( listenPort uint16, options ...Option, ) *SynScanner { - ctx, cancel := context.WithCancel(context.Background()) - scanner := &SynScanner{ - ctx: ctx, - cancel: cancel, targets: targets, networkInfo: networkInfo, cap: &defaultPacketCapture{}, @@ -140,6 +134,7 @@ func (s *SynScanner) Scan() error { s.scanningMux.Unlock() s.handle = handle + s.cancel = make(chan struct{}) go s.readPackets() @@ -172,7 +167,9 @@ func (s *SynScanner) Scan() error { // Stop stops the scanner func (s *SynScanner) Stop() { - s.cancel() + if s.cancel != nil { + close(s.cancel) + } if s.handle != nil { s.handle.Close() @@ -212,12 +209,12 @@ func (s *SynScanner) SetTargets(targets []*ArpScanResult) { } func (s *SynScanner) readPackets() { - stopChan := make(chan struct{}) + done := make(chan struct{}) go func() { for { select { - case <-stopChan: + case <-done: return default: var eth layers.Ethernet @@ -260,15 +257,13 @@ func (s *SynScanner) readPackets() { }() defer func() { - go func() { - stopChan <- struct{}{} - }() + go close(done) s.reset() }() for { select { - case <-s.ctx.Done(): + case <-s.cancel: return default: s.packetSentAtMux.RLock() @@ -409,10 +404,4 @@ func (s *SynScanner) reset() { s.packetSentAtMux.Lock() s.lastPacketSentAt = time.Time{} s.packetSentAtMux.Unlock() - - if s.ctx.Err() != nil { - ctx, cancel := context.WithCancel(context.Background()) - s.ctx = ctx - s.cancel = cancel - } }