- domain entries covered by domain_suffix are removed - domain/suffix entries covered by domain_keyword are removed - child suffixes covered by parent suffix are removed - adjacent/contained CIDRs are merged into larger blocks - Available via --optimize/-O flag on merge and generate commands - cn-direct: 634 -> 587 rules (-7.4%)
106 lines
2.4 KiB
Go
106 lines
2.4 KiB
Go
package cli
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"rulekit/internal/engine"
|
|
"rulekit/internal/model"
|
|
"text/tabwriter"
|
|
|
|
"github.com/spf13/cobra"
|
|
)
|
|
|
|
func init() {
|
|
mergeCmd := &cobra.Command{
|
|
Use: "merge <category>",
|
|
Short: "Merge rules from all sources for a category",
|
|
Args: cobra.ExactArgs(1),
|
|
RunE: runMerge,
|
|
}
|
|
mergeCmd.Flags().IntP("limit", "n", 0, "limit output rows (0 = all)")
|
|
mergeCmd.Flags().BoolP("stats", "s", false, "show statistics only")
|
|
mergeCmd.Flags().BoolP("optimize", "O", false, "optimize: merge covered domains, aggregate CIDRs")
|
|
rootCmd.AddCommand(mergeCmd)
|
|
}
|
|
|
|
func runMerge(cmd *cobra.Command, args []string) error {
|
|
categoryName := args[0]
|
|
limit, _ := cmd.Flags().GetInt("limit")
|
|
statsOnly, _ := cmd.Flags().GetBool("stats")
|
|
optimize, _ := cmd.Flags().GetBool("optimize")
|
|
|
|
cfg := loadConfig()
|
|
|
|
var merged *model.MergedRuleSet
|
|
var optResult *engine.OptimizeResult
|
|
|
|
if optimize {
|
|
var err error
|
|
merged, optResult, err = engine.MergeOptimized(cfg, categoryName)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
} else {
|
|
var err error
|
|
merged, err = engine.Merge(cfg, categoryName)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// Stats
|
|
types := map[model.RuleType]int{}
|
|
for _, r := range merged.Rules {
|
|
types[r.Type]++
|
|
}
|
|
|
|
fmt.Printf("Merged: %s (%d rules)\n", merged.Name, len(merged.Rules))
|
|
if optResult != nil {
|
|
fmt.Printf("Optimized: %d -> %d (-%d domains by suffix, -%d by keyword, -%d CIDRs merged)\n",
|
|
optResult.Before, optResult.After,
|
|
optResult.DomainsMerged, optResult.KeywordMerged, optResult.CIDRsMerged)
|
|
}
|
|
for t, c := range types {
|
|
fmt.Printf(" %s: %d\n", t, c)
|
|
}
|
|
|
|
// Provenance summary
|
|
sourceCounts := map[string]int{}
|
|
for _, sources := range merged.Provenance {
|
|
for _, s := range sources {
|
|
sourceCounts[s]++
|
|
}
|
|
}
|
|
fmt.Printf("\nContributions:\n")
|
|
for src, count := range sourceCounts {
|
|
fmt.Printf(" %s: %d rules\n", src, count)
|
|
}
|
|
|
|
if statsOnly {
|
|
return nil
|
|
}
|
|
|
|
fmt.Println()
|
|
w := tabwriter.NewWriter(os.Stdout, 0, 4, 2, ' ', 0)
|
|
fmt.Fprintf(w, "TYPE\tVALUE\tSOURCES\n")
|
|
fmt.Fprintf(w, "----\t-----\t-------\n")
|
|
for i, r := range merged.Rules {
|
|
if limit > 0 && i >= limit {
|
|
fmt.Fprintf(w, "... (%d more)\n", len(merged.Rules)-limit)
|
|
break
|
|
}
|
|
sources := merged.Provenance[r.Key()]
|
|
srcStr := ""
|
|
for j, s := range sources {
|
|
if j > 0 {
|
|
srcStr += ", "
|
|
}
|
|
srcStr += s
|
|
}
|
|
fmt.Fprintf(w, "%s\t%s\t%s\n", r.Type, r.Value, srcStr)
|
|
}
|
|
w.Flush()
|
|
|
|
return nil
|
|
}
|