github.com/chainreactors/fingers@v1.2.1/cmd/validate/main.go (about)

     1  package main
     2  
     3  import (
     4  	"encoding/json"
     5  	"flag"
     6  	"fmt"
     7  	"io/ioutil"
     8  	"os"
     9  	"path/filepath"
    10  
    11  	"github.com/chainreactors/fingers/alias"
    12  	"github.com/chainreactors/fingers/fingers"
    13  	"github.com/invopop/jsonschema"
    14  	"gopkg.in/yaml.v3"
    15  )
    16  
    17  func main() {
    18  	var (
    19  		engine = flag.String("engine", "fingers", "Engine type to validate (fingers, alias)")
    20  		schema = flag.Bool("schema", false, "Output JSON schema for the specified engine")
    21  		help   = flag.Bool("help", false, "Show help information")
    22  	)
    23  	
    24  	flag.Parse()
    25  
    26  	if *help {
    27  		showHelp()
    28  		return
    29  	}
    30  
    31  	// Handle schema output
    32  	if *schema {
    33  		if err := outputSchemaForEngine(*engine); err != nil {
    34  			fmt.Printf("Error generating schema: %s\n", err.Error())
    35  			os.Exit(1)
    36  		}
    37  		return
    38  	}
    39  
    40  	if len(flag.Args()) == 0 {
    41  		showHelp()
    42  		return
    43  	}
    44  
    45  	target := flag.Args()[0]
    46  
    47  	// Validate files
    48  	err := filepath.Walk(target, func(path string, info os.FileInfo, err error) error {
    49  		if err != nil {
    50  			return err
    51  		}
    52  
    53  		// Check for supported file extensions
    54  		ext := filepath.Ext(path)
    55  		if ext != ".yaml" && ext != ".yml" && ext != ".json" {
    56  			return nil
    57  		}
    58  
    59  		fmt.Printf("Validating: %s\n", path)
    60  
    61  		// Read file content
    62  		content, err := ioutil.ReadFile(path)
    63  		if err != nil {
    64  			fmt.Printf("❌ %s - Failed to read file: %s\n", path, err.Error())
    65  			return nil
    66  		}
    67  
    68  		// Convert to JSON for validation
    69  		var data interface{}
    70  		if ext == ".json" {
    71  			err = json.Unmarshal(content, &data)
    72  		} else {
    73  			err = yaml.Unmarshal(content, &data)
    74  		}
    75  		
    76  		if err != nil {
    77  			fmt.Printf("❌ %s - Failed to parse file: %s\n", path, err.Error())
    78  			return nil
    79  		}
    80  
    81  		// Convert to JSON bytes for schema validation
    82  		jsonData, err := json.Marshal(data)
    83  		if err != nil {
    84  			fmt.Printf("❌ %s - Failed to convert to JSON: %s\n", path, err.Error())
    85  			return nil
    86  		}
    87  
    88  		// Validate against schema based on engine type
    89  		var validCount, totalCount int
    90  		var results []ValidationResult
    91  		
    92  		switch *engine {
    93  		case "fingers":
    94  			validCount, totalCount, results = validateFingersSchema(jsonData)
    95  		case "alias":
    96  			validCount, totalCount, results = validateAliasSchema(jsonData)
    97  		default:
    98  			fmt.Printf("❌ Unsupported engine type: %s\n", *engine)
    99  			return nil
   100  		}
   101  		
   102  		// Print detailed results
   103  		fmt.Printf("📁 %s (%d items)\n", path, totalCount)
   104  		
   105  		for _, result := range results {
   106  			if result.Valid {
   107  				fmt.Printf("  ✅ %s - Valid\n", result.Name)
   108  			} else {
   109  				fmt.Printf("  ❌ %s - %s\n", result.Name, result.Error)
   110  			}
   111  		}
   112  		
   113  		// Print summary
   114  		if validCount == totalCount && totalCount > 0 {
   115  			fmt.Printf("  📊 Summary: All %d items valid\n\n", totalCount)
   116  		} else if validCount > 0 {
   117  			fmt.Printf("  📊 Summary: %d/%d items valid\n\n", validCount, totalCount)
   118  		} else {
   119  			fmt.Printf("  📊 Summary: No valid items\n\n")
   120  		}
   121  		return nil
   122  	})
   123  
   124  	if err != nil {
   125  		fmt.Printf("Error during validation: %s\n", err.Error())
   126  		os.Exit(1)
   127  	}
   128  }
   129  
   130  func showHelp() {
   131  	fmt.Println("Fingers Fingerprint Library Validator")
   132  	fmt.Println()
   133  	fmt.Println("Usage:")
   134  	fmt.Println("  validate <path_or_file> [options]")
   135  	fmt.Println()
   136  	fmt.Println("Options:")
   137  	fmt.Println("  -engine string")
   138  	fmt.Println("        Engine type to validate (fingers, alias) (default \"fingers\")")
   139  	fmt.Println("  -schema")
   140  	fmt.Println("        Output JSON schema for the specified engine")
   141  	fmt.Println("  -help")
   142  	fmt.Println("        Show this help information")
   143  	fmt.Println()
   144  	fmt.Println("Examples:")
   145  	fmt.Println("  # Validate fingers fingerprint files")
   146  	fmt.Println("  validate fingerprints/ -engine fingers")
   147  	fmt.Println()
   148  	fmt.Println("  # Validate alias files")
   149  	fmt.Println("  validate aliases/ -engine alias")
   150  	fmt.Println()
   151  	fmt.Println("  # Output JSON schema for fingers")
   152  	fmt.Println("  validate -schema -engine fingers")
   153  	fmt.Println()
   154  	fmt.Println("  # Output JSON schema for alias")
   155  	fmt.Println("  validate -schema -engine alias")
   156  }
   157  
   158  type ValidationResult struct {
   159  	Name  string
   160  	Valid bool
   161  	Error string
   162  }
   163  
   164  // Validate fingers fingerprints
   165  func validateFingersSchema(jsonData []byte) (validCount, totalCount int, results []ValidationResult) {
   166  	// First try as single fingerprint
   167  	var singleFinger fingers.Finger
   168  	if err := json.Unmarshal(jsonData, &singleFinger); err == nil {
   169  		// Single fingerprint case
   170  		totalCount = 1
   171  		result := ValidationResult{
   172  			Name:  singleFinger.Name,
   173  			Valid: true,
   174  		}
   175  		
   176  		if singleFinger.Name == "" {
   177  			result.Name = "<unnamed>"
   178  			result.Valid = false
   179  			result.Error = "name is required"
   180  		} else if len(singleFinger.Rules) == 0 {
   181  			result.Valid = false
   182  			result.Error = "must have at least one rule"
   183  		}
   184  		
   185  		if result.Valid {
   186  			validCount = 1
   187  		}
   188  		results = append(results, result)
   189  		return
   190  	}
   191  
   192  	// Try as array of fingerprints
   193  	var fingerArray []fingers.Finger
   194  	if err := json.Unmarshal(jsonData, &fingerArray); err == nil {
   195  		// Array of fingerprints case
   196  		totalCount = len(fingerArray)
   197  		for i, finger := range fingerArray {
   198  			result := ValidationResult{
   199  				Name:  finger.Name,
   200  				Valid: true,
   201  			}
   202  			
   203  			if finger.Name == "" {
   204  				result.Name = fmt.Sprintf("<unnamed-%d>", i)
   205  				result.Valid = false
   206  				result.Error = "name is required"
   207  			} else if len(finger.Rules) == 0 {
   208  				result.Valid = false
   209  				result.Error = "must have at least one rule"
   210  			}
   211  			
   212  			if result.Valid {
   213  				validCount++
   214  			}
   215  			results = append(results, result)
   216  		}
   217  		return
   218  	}
   219  
   220  	// If neither single nor array format works
   221  	results = append(results, ValidationResult{
   222  		Name:  "<invalid>",
   223  		Valid: false,
   224  		Error: "data doesn't match fingers fingerprint format",
   225  	})
   226  	totalCount = 1
   227  	return
   228  }
   229  
   230  // Validate alias entries
   231  func validateAliasSchema(jsonData []byte) (validCount, totalCount int, results []ValidationResult) {
   232  	// First try as single alias
   233  	var singleAlias alias.Alias
   234  	if err := json.Unmarshal(jsonData, &singleAlias); err == nil {
   235  		// Single alias case
   236  		totalCount = 1
   237  		result := ValidationResult{
   238  			Name:  singleAlias.Name,
   239  			Valid: true,
   240  		}
   241  		
   242  		if singleAlias.Name == "" {
   243  			result.Name = "<unnamed>"
   244  			result.Valid = false
   245  			result.Error = "name is required"
   246  		} else if len(singleAlias.AliasMap) == 0 {
   247  			result.Valid = false
   248  			result.Error = "must have at least one alias mapping"
   249  		} else if singleAlias.Priority < 0 || singleAlias.Priority > 5 {
   250  			result.Valid = false
   251  			result.Error = "priority must be between 0 and 5"
   252  		}
   253  		
   254  		if result.Valid {
   255  			validCount = 1
   256  		}
   257  		results = append(results, result)
   258  		return
   259  	}
   260  
   261  	// Try as array of aliases
   262  	var aliasArray []alias.Alias
   263  	if err := json.Unmarshal(jsonData, &aliasArray); err == nil {
   264  		// Array of aliases case
   265  		totalCount = len(aliasArray)
   266  		for i, a := range aliasArray {
   267  			result := ValidationResult{
   268  				Name:  a.Name,
   269  				Valid: true,
   270  			}
   271  			
   272  			if a.Name == "" {
   273  				result.Name = fmt.Sprintf("<unnamed-%d>", i)
   274  				result.Valid = false
   275  				result.Error = "name is required"
   276  			} else if len(a.AliasMap) == 0 {
   277  				result.Valid = false
   278  				result.Error = "must have at least one alias mapping"
   279  			} else if a.Priority < 0 || a.Priority > 5 {
   280  				result.Valid = false
   281  				result.Error = "priority must be between 0 and 5"
   282  			}
   283  			
   284  			if result.Valid {
   285  				validCount++
   286  			}
   287  			results = append(results, result)
   288  		}
   289  		return
   290  	}
   291  
   292  	// If neither single nor array format works
   293  	results = append(results, ValidationResult{
   294  		Name:  "<invalid>",
   295  		Valid: false,
   296  		Error: "data doesn't match alias format",
   297  	})
   298  	totalCount = 1
   299  	return
   300  }
   301  
   302  func outputSchemaForEngine(engineType string) error {
   303  	reflector := jsonschema.Reflector{
   304  		AllowAdditionalProperties: false,
   305  		DoNotReference:           true,
   306  	}
   307  
   308  	var schema *jsonschema.Schema
   309  	
   310  	switch engineType {
   311  	case "fingers":
   312  		schema = reflector.Reflect(&fingers.Finger{})
   313  		schema.Title = "Fingers Fingerprint Schema"
   314  		schema.Description = "JSON Schema for validating Fingers fingerprint library format"
   315  		
   316  		// Add examples
   317  		schema.Examples = []interface{}{
   318  			map[string]interface{}{
   319  				"name":     "nginx",
   320  				"vendor":   "nginx",
   321  				"product":  "nginx",
   322  				"protocol": "http",
   323  				"link":     "https://nginx.org",
   324  				"default_port": []string{"80", "443"},
   325  				"focus":    false,
   326  				"rule": []map[string]interface{}{
   327  					{
   328  						"regexps": map[string]interface{}{
   329  							"header": []string{"Server: nginx"},
   330  							"regexp": []string{"nginx/([\\d\\.]+)"},
   331  						},
   332  						"version": "\\1",
   333  						"level":   0,
   334  					},
   335  				},
   336  				"tag":   []string{"web", "server"},
   337  				"opsec": false,
   338  			},
   339  		}
   340  		
   341  	case "alias":
   342  		schema = reflector.Reflect(&alias.Alias{})
   343  		schema.Title = "Alias Schema"
   344  		schema.Description = "JSON Schema for validating alias mapping format"
   345  		
   346  		// Add examples
   347  		schema.Examples = []interface{}{
   348  			map[string]interface{}{
   349  				"name":     "nginx",
   350  				"vendor":   "nginx",
   351  				"product":  "nginx",
   352  				"label":    "web,server,proxy",
   353  				"priority": 1,
   354  				"target":   []string{"https://nginx.org", "192.168.1.100:80"},
   355  				"alias": map[string]interface{}{
   356  					"wappalyzer": []string{"Nginx"},
   357  					"ehole":      []string{"nginx"},
   358  					"fingers":    []string{"nginx"},
   359  				},
   360  				"block": []string{},
   361  			},
   362  		}
   363  		
   364  	default:
   365  		return fmt.Errorf("unsupported engine type: %s", engineType)
   366  	}
   367  
   368  	schemaJSON, err := json.MarshalIndent(schema, "", "  ")
   369  	if err != nil {
   370  		return err
   371  	}
   372  
   373  	fmt.Println(string(schemaJSON))
   374  	return nil
   375  }