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 }