diff --git a/internal/scans/postgres_store.go b/internal/scans/postgres_store.go index 4c5674d..2a7df22 100644 --- a/internal/scans/postgres_store.go +++ b/internal/scans/postgres_store.go @@ -24,12 +24,13 @@ func NewPostgresStore(db *sql.DB) *PostgresStore { } func (s *PostgresStore) CreateScan(req CreateScanRequest) (ScanJob, error) { + cidrSnapshot := append([]string(nil), req.CIDRs...) if err := validateCreateScanRequest(&req); err != nil { return ScanJob{}, err } applyDefaultOptions(&req.Options) - id, err := newID() + id, err := newScanID(cidrSnapshot) if err != nil { return ScanJob{}, err } diff --git a/internal/scans/scan_id_test.go b/internal/scans/scan_id_test.go new file mode 100644 index 0000000..e9fbed0 --- /dev/null +++ b/internal/scans/scan_id_test.go @@ -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) + } +} diff --git a/internal/scans/store.go b/internal/scans/store.go index 096be83..035588a 100644 --- a/internal/scans/store.go +++ b/internal/scans/store.go @@ -6,6 +6,7 @@ import ( "errors" "net" "sort" + "strings" "sync" "time" ) @@ -161,13 +162,14 @@ func NewMemoryStore() *MemoryStore { } func (s *MemoryStore) CreateScan(req CreateScanRequest) (ScanJob, error) { + cidrSnapshot := append([]string(nil), req.CIDRs...) if err := validateCreateScanRequest(&req); err != nil { return ScanJob{}, err } applyDefaultOptions(&req.Options) - id, err := newID() + id, err := newScanID(cidrSnapshot) if err != nil { return ScanJob{}, err } @@ -546,12 +548,42 @@ func validateCreateScanRequest(req *CreateScanRequest) error { return nil } -func newID() (string, error) { - var b [16]byte - if _, err := rand.Read(b[:]); err != nil { +// newScanID — человекочитаемый scan_id: UTC-время с наносекундами + CIDR из запроса (как ввёл пользователь, до expand) + 4 hex (уникальность при коллизии по времени/CIDR). +func newScanID(cidrLabels []string) (string, error) { + var tail [2]byte + if _, err := rand.Read(tail[:]); err != nil { 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 {