package pipeline import ( "bufio" "encoding/json" "fmt" "net/netip" "strings" ) // Защита от pathological JSON: глубокая рекурсия при обходе и взрыв числа узлов по пути. const ( maxJSONWalkDepth = 512 maxJSONPathBreadth = 50000 ) // ParseCIDRLines extracts unique IPv4/IPv6 CIDRs from plain text (one per line, # comments, empty lines skipped). func ParseCIDRLines(body string) []netip.Prefix { seen := make(map[string]struct{}) var out []netip.Prefix sc := bufio.NewScanner(strings.NewReader(body)) for sc.Scan() { line := strings.TrimSpace(sc.Text()) if line == "" || strings.HasPrefix(line, "#") { continue } pfx := parseOneCIDR(line) if !pfx.IsValid() { continue } m := pfx.Masked() s := m.String() if _, ok := seen[s]; ok { continue } seen[s] = struct{}{} out = append(out, m) } return out } // ExtractCIDRs parses CIDR prefixes from plaintext lines or a JSON payload (see sourceKind and prefixPath). // For sourceKind="json", prefixPath supports dotted traversal, with [] for arrays: // e.g. "prefixes[]", "data.items[].cidr". func ExtractCIDRs(body, sourceKind, prefixPath string) ([]netip.Prefix, error) { if strings.EqualFold(strings.TrimSpace(sourceKind), "json") { return parseCIDRsFromJSON(body, prefixPath) } return ParseCIDRLines(body), nil } func parseCIDRsFromJSON(body, prefixPath string) ([]netip.Prefix, error) { var root any if err := json.Unmarshal([]byte(body), &root); err != nil { return nil, err } values, err := jsonValuesAtPath(root, prefixPath) if err != nil { return nil, err } seen := make(map[string]struct{}) var out []netip.Prefix for _, raw := range values { pfx := parseOneCIDR(raw) if !pfx.IsValid() { continue } m := pfx.Masked() s := m.String() if _, ok := seen[s]; ok { continue } seen[s] = struct{}{} out = append(out, m) } return out, nil } func jsonValuesAtPath(root any, prefixPath string) ([]string, error) { path := strings.TrimSpace(prefixPath) if path == "" { return flattenJSONStrings(root, 0) } parts := strings.Split(path, ".") nodes := []any{root} for _, p := range parts { part := strings.TrimSpace(p) if part == "" { continue } iter := strings.HasSuffix(part, "[]") key := strings.TrimSuffix(part, "[]") var next []any for _, n := range nodes { obj, ok := n.(map[string]any) if !ok { continue } child, ok := obj[key] if !ok { continue } if iter { if arr, ok := child.([]any); ok { next = append(next, arr...) } continue } next = append(next, child) } if len(next) > maxJSONPathBreadth { return nil, fmt.Errorf("json path: слишком много узлов на шаге (>%d)", maxJSONPathBreadth) } nodes = next if len(nodes) == 0 { return nil, nil } } var out []string for _, n := range nodes { part, err := flattenJSONStrings(n, 0) if err != nil { return nil, err } out = append(out, part...) } return out, nil } func flattenJSONStrings(v any, depth int) ([]string, error) { if depth > maxJSONWalkDepth { return nil, fmt.Errorf("json: глубина вложенности превышает %d", maxJSONWalkDepth) } switch x := v.(type) { case string: return []string{strings.TrimSpace(x)}, nil case []any: var out []string for _, item := range x { part, err := flattenJSONStrings(item, depth+1) if err != nil { return nil, err } out = append(out, part...) } return out, nil case map[string]any: var out []string for _, item := range x { part, err := flattenJSONStrings(item, depth+1) if err != nil { return nil, err } out = append(out, part...) } return out, nil default: return nil, nil } } func parseOneCIDR(s string) netip.Prefix { if p, err := netip.ParsePrefix(s); err == nil { return p } if addr, err := netip.ParseAddr(s); err == nil { if addr.Is4() { p, _ := addr.Prefix(32) return p } p, _ := addr.Prefix(128) return p } return netip.Prefix{} }