feat: human-readable scan_id from UTC time + request CIDRs + short random suffix
Made-with: Cursor
This commit is contained in:
@@ -24,12 +24,13 @@ func NewPostgresStore(db *sql.DB) *PostgresStore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *PostgresStore) CreateScan(req CreateScanRequest) (ScanJob, error) {
|
func (s *PostgresStore) CreateScan(req CreateScanRequest) (ScanJob, error) {
|
||||||
|
cidrSnapshot := append([]string(nil), req.CIDRs...)
|
||||||
if err := validateCreateScanRequest(&req); err != nil {
|
if err := validateCreateScanRequest(&req); err != nil {
|
||||||
return ScanJob{}, err
|
return ScanJob{}, err
|
||||||
}
|
}
|
||||||
applyDefaultOptions(&req.Options)
|
applyDefaultOptions(&req.Options)
|
||||||
|
|
||||||
id, err := newID()
|
id, err := newScanID(cidrSnapshot)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ScanJob{}, err
|
return ScanJob{}, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
package scans
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNewScanIDShape(t *testing.T) {
|
||||||
|
id, err := newScanID([]string{"192.168.160.0/24"})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
parts := strings.Split(id, "_")
|
||||||
|
if len(parts) < 3 {
|
||||||
|
t.Fatalf("expected at least time_slug_suffix, got %q", id)
|
||||||
|
}
|
||||||
|
if len(parts[len(parts)-1]) != 4 {
|
||||||
|
t.Fatalf("expected 4-hex suffix, got %q", id)
|
||||||
|
}
|
||||||
|
if !strings.Contains(id, "192.168.160.0_24") {
|
||||||
|
t.Fatalf("expected sanitized cidr in id: %q", id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSanitizeScanSlug(t *testing.T) {
|
||||||
|
if g := sanitizeScanSlug("10.0.0.0/8"); g != "10.0.0.0_8" {
|
||||||
|
t.Fatalf("got %q", g)
|
||||||
|
}
|
||||||
|
}
|
||||||
+37
-5
@@ -6,6 +6,7 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"net"
|
"net"
|
||||||
"sort"
|
"sort"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
@@ -161,13 +162,14 @@ func NewMemoryStore() *MemoryStore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *MemoryStore) CreateScan(req CreateScanRequest) (ScanJob, error) {
|
func (s *MemoryStore) CreateScan(req CreateScanRequest) (ScanJob, error) {
|
||||||
|
cidrSnapshot := append([]string(nil), req.CIDRs...)
|
||||||
if err := validateCreateScanRequest(&req); err != nil {
|
if err := validateCreateScanRequest(&req); err != nil {
|
||||||
return ScanJob{}, err
|
return ScanJob{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
applyDefaultOptions(&req.Options)
|
applyDefaultOptions(&req.Options)
|
||||||
|
|
||||||
id, err := newID()
|
id, err := newScanID(cidrSnapshot)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ScanJob{}, err
|
return ScanJob{}, err
|
||||||
}
|
}
|
||||||
@@ -546,12 +548,42 @@ func validateCreateScanRequest(req *CreateScanRequest) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func newID() (string, error) {
|
// newScanID — человекочитаемый scan_id: UTC-время с наносекундами + CIDR из запроса (как ввёл пользователь, до expand) + 4 hex (уникальность при коллизии по времени/CIDR).
|
||||||
var b [16]byte
|
func newScanID(cidrLabels []string) (string, error) {
|
||||||
if _, err := rand.Read(b[:]); err != nil {
|
var tail [2]byte
|
||||||
|
if _, err := rand.Read(tail[:]); err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
return hex.EncodeToString(b[:]), nil
|
sfx := hex.EncodeToString(tail[:])
|
||||||
|
ts := time.Now().UTC().Format("20060102T150405.000000000")
|
||||||
|
raw := strings.TrimSpace(strings.Join(cidrLabels, "+"))
|
||||||
|
if raw == "" {
|
||||||
|
raw = "scan"
|
||||||
|
}
|
||||||
|
slug := sanitizeScanSlug(raw)
|
||||||
|
const maxSlug = 180
|
||||||
|
if len(slug) > maxSlug {
|
||||||
|
slug = slug[:maxSlug]
|
||||||
|
}
|
||||||
|
return ts + "_" + slug + "_" + sfx, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func sanitizeScanSlug(s string) string {
|
||||||
|
var b strings.Builder
|
||||||
|
b.Grow(len(s))
|
||||||
|
for _, r := range s {
|
||||||
|
switch {
|
||||||
|
case r >= 'a' && r <= 'z' || r >= 'A' && r <= 'Z' || r >= '0' && r <= '9':
|
||||||
|
b.WriteRune(r)
|
||||||
|
case r == '.', r == '-', r == '_':
|
||||||
|
b.WriteRune(r)
|
||||||
|
case r == '/', r == ':', r == ',', r == '+', r == ' ', r == '\\':
|
||||||
|
b.WriteByte('_')
|
||||||
|
default:
|
||||||
|
b.WriteByte('_')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return strings.Trim(strings.TrimSpace(b.String()), "_")
|
||||||
}
|
}
|
||||||
|
|
||||||
func dedupeHostsLatest(items []HostResult, limit int) []HostResult {
|
func dedupeHostsLatest(items []HostResult, limit int) []HostResult {
|
||||||
|
|||||||
Reference in New Issue
Block a user