Initial commit: IT Nexus Web-App
This commit is contained in:
106
nexus-scanner/internal/modules/portscan/portscan.go
Normal file
106
nexus-scanner/internal/modules/portscan/portscan.go
Normal file
@@ -0,0 +1,106 @@
|
||||
// Package portscan performs concurrent TCP port scanning on all online hosts.
|
||||
package portscan
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net"
|
||||
"sort"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/cereda-systems/nexus-scanner/internal/config"
|
||||
"github.com/cereda-systems/nexus-scanner/internal/db"
|
||||
)
|
||||
|
||||
const modName = "port_scan"
|
||||
|
||||
// Common ports to scan when none are configured.
|
||||
var defaultPorts = []int{21, 22, 23, 25, 53, 80, 110, 135, 139, 143, 443, 445, 3389, 5985, 8080, 8443, 9100}
|
||||
|
||||
// Module scans TCP ports on all online hosts.
|
||||
type Module struct {
|
||||
site string
|
||||
cfg config.PortScanConfig
|
||||
store *db.Store
|
||||
}
|
||||
|
||||
// New creates a new port scan module.
|
||||
func New(site string, cfg config.PortScanConfig, store *db.Store) *Module {
|
||||
return &Module{site: site, cfg: cfg, store: store}
|
||||
}
|
||||
|
||||
func (m *Module) Name() string { return modName }
|
||||
func (m *Module) Interval() time.Duration { return m.cfg.Interval }
|
||||
|
||||
// Run scans all online hosts and stores their open ports.
|
||||
func (m *Module) Run(ctx context.Context) error {
|
||||
hosts, err := m.store.ListHosts("")
|
||||
if err != nil {
|
||||
return fmt.Errorf("list hosts: %w", err)
|
||||
}
|
||||
|
||||
ports := m.cfg.Ports
|
||||
if len(ports) == 0 {
|
||||
ports = defaultPorts
|
||||
}
|
||||
timeout := m.cfg.Timeout
|
||||
if timeout == 0 {
|
||||
timeout = 500 * time.Millisecond
|
||||
}
|
||||
|
||||
scanID, err := m.store.BeginScan(modName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("begin scan: %w", err)
|
||||
}
|
||||
|
||||
found := 0
|
||||
for _, h := range hosts {
|
||||
if h.Status != "online" {
|
||||
continue
|
||||
}
|
||||
open := scanHost(ctx, h.IP, ports, timeout)
|
||||
sort.Ints(open)
|
||||
if err := m.store.UpsertHostPorts(h.IP, open); err != nil {
|
||||
slog.Error("save host ports", "ip", h.IP, "err", err)
|
||||
}
|
||||
if len(open) > 0 {
|
||||
slog.Debug("port scan", "ip", h.IP, "open", open)
|
||||
found++
|
||||
}
|
||||
}
|
||||
|
||||
_ = m.store.EndScan(scanID, nil)
|
||||
slog.Info("port scan complete", "hosts_with_open_ports", found, "total_hosts", len(hosts))
|
||||
return nil
|
||||
}
|
||||
|
||||
// scanHost tries all ports concurrently (max 30 goroutines) and returns open ones.
|
||||
func scanHost(ctx context.Context, ip string, ports []int, timeout time.Duration) []int {
|
||||
var mu sync.Mutex
|
||||
var open []int
|
||||
var wg sync.WaitGroup
|
||||
sem := make(chan struct{}, 30)
|
||||
|
||||
for _, port := range ports {
|
||||
wg.Add(1)
|
||||
go func(p int) {
|
||||
defer wg.Done()
|
||||
sem <- struct{}{}
|
||||
defer func() { <-sem }()
|
||||
|
||||
addr := fmt.Sprintf("%s:%d", ip, p)
|
||||
d := net.Dialer{Timeout: timeout}
|
||||
conn, err := d.DialContext(ctx, "tcp", addr)
|
||||
if err == nil {
|
||||
conn.Close()
|
||||
mu.Lock()
|
||||
open = append(open, p)
|
||||
mu.Unlock()
|
||||
}
|
||||
}(port)
|
||||
}
|
||||
wg.Wait()
|
||||
return open
|
||||
}
|
||||
Reference in New Issue
Block a user