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

     1  package main
     2  
     3  import (
     4  	"encoding/json"
     5  	"flag"
     6  	"fmt"
     7  	"io/ioutil"
     8  	"net/http"
     9  	"os"
    10  	"strings"
    11  	"time"
    12  
    13  	"github.com/chainreactors/fingers"
    14  	"github.com/chainreactors/fingers/alias"
    15  	"github.com/chainreactors/fingers/common"
    16  	"gopkg.in/yaml.v3"
    17  )
    18  
    19  func main() {
    20  	var (
    21  		aliasFile = flag.String("alias", "", "Path to alias file")
    22  		target    = flag.String("target", "", "Target URL or address to test (overrides alias targets if provided)")
    23  		aliasName = flag.String("name", "", "Filter to test only specific alias by name")
    24  		timeout   = flag.Int("timeout", 10, "Request timeout in seconds")
    25  		verbose   = flag.Bool("verbose", false, "Enable verbose output")
    26  		detectAll = flag.Bool("detect-all", false, "Run general fingerprint detection on target")
    27  		help      = flag.Bool("help", false, "Show help information")
    28  	)
    29  
    30  	flag.Parse()
    31  
    32  	if *help {
    33  		showHelp()
    34  		return
    35  	}
    36  
    37  	if *target == "" && *aliasFile == "" {
    38  		fmt.Println("Error: At least one of -target or -alias is required")
    39  		showHelp()
    40  		os.Exit(1)
    41  	}
    42  
    43  	if *detectAll && *target == "" {
    44  		fmt.Println("Error: -detect-all requires -target to be specified")
    45  		os.Exit(1)
    46  	}
    47  
    48  	// Initialize fingerprint engine
    49  	eng, err := fingers.NewEngine()
    50  	if err != nil {
    51  		fmt.Printf("Error initializing engine: %s\n", err.Error())
    52  		os.Exit(1)
    53  	}
    54  
    55  	if *target != "" {
    56  		fmt.Printf("🎯 Testing target: %s\n", *target)
    57  	}
    58  
    59  	// If detect-all flag is set, run general detection
    60  	if *detectAll {
    61  		fmt.Println("🔍 Running general fingerprint detection...")
    62  		allResults := runGeneralDetection(eng, *target, *timeout, *verbose)
    63  
    64  		fmt.Printf("📊 General Detection Results:\n")
    65  		if len(allResults) > 0 {
    66  			fmt.Printf("   Found %d fingerprints:\n", len(allResults))
    67  			for _, result := range allResults {
    68  				fmt.Printf("   🎯 %s (%s) - %s\n", result.Name, result.From, result.Version)
    69  			}
    70  		} else {
    71  			fmt.Println("   ❌ No fingerprints detected")
    72  		}
    73  		fmt.Println()
    74  	}
    75  
    76  	// Load and test aliases if alias file provided
    77  	if *aliasFile != "" {
    78  		aliases, err := loadAliasFile(*aliasFile)
    79  		if err != nil {
    80  			fmt.Printf("Error loading alias file: %s\n", err.Error())
    81  			os.Exit(1)
    82  		}
    83  
    84  		fmt.Printf("📁 Loaded %d aliases from %s\n", len(aliases), *aliasFile)
    85  		if *aliasName != "" {
    86  			fmt.Printf("🎯 Filtering for alias: %s\n", *aliasName)
    87  		}
    88  		if *target != "" {
    89  			fmt.Printf("🔄 Using target override: %s\n", *target)
    90  		}
    91  		fmt.Println()
    92  		testAliasMatching(eng, *target, aliases, *aliasName, *timeout, *verbose)
    93  	}
    94  }
    95  
    96  func runGeneralDetection(eng *fingers.Engine, target string, timeout int, verbose bool) []common.Framework {
    97  	// Create HTTP client with timeout
    98  	client := &http.Client{
    99  		Timeout: time.Duration(timeout) * time.Second,
   100  	}
   101  
   102  	// Ensure target has protocol
   103  	if !strings.HasPrefix(target, "http://") && !strings.HasPrefix(target, "https://") {
   104  		target = "http://" + target
   105  	}
   106  
   107  	if verbose {
   108  		fmt.Printf("      🌐 Requesting: %s\n", target)
   109  	}
   110  
   111  	// Make HTTP request
   112  	resp, err := client.Get(target)
   113  	if err != nil {
   114  		if verbose {
   115  			fmt.Printf("      ❌ Request failed: %s\n", err.Error())
   116  		}
   117  		return nil
   118  	}
   119  	defer resp.Body.Close()
   120  
   121  	// Detect frameworks
   122  	frameworks, err := eng.DetectResponse(resp)
   123  	if err != nil {
   124  		if verbose {
   125  			fmt.Printf("      ❌ Detection failed: %s\n", err.Error())
   126  		}
   127  		return nil
   128  	}
   129  
   130  	// Convert map to slice
   131  	var results []common.Framework
   132  	for _, framework := range frameworks {
   133  		results = append(results, *framework)
   134  	}
   135  
   136  	return results
   137  }
   138  
   139  func testAliasMatching(eng *fingers.Engine, target string, aliases []alias.Alias, filterName string, timeout int, verbose bool) {
   140  	// Test each alias against the target
   141  	var totalTests, successfulMatches int
   142  	for _, aliasEntry := range aliases {
   143  		// Filter by name if specified
   144  		if filterName != "" && aliasEntry.Name != filterName {
   145  			if verbose {
   146  				fmt.Printf("⏭️  Skipping %s - name filter doesn't match\n", aliasEntry.Name)
   147  			}
   148  			continue
   149  		}
   150  
   151  		// Determine target to test
   152  		testTarget := target
   153  		if target == "" {
   154  			// Use alias targets if no target override provided
   155  			if len(aliasEntry.Link) == 0 {
   156  				if verbose {
   157  					fmt.Printf("⏭️  Skipping %s - no target URLs defined\n", aliasEntry.Name)
   158  				}
   159  				continue
   160  			}
   161  			// Use first target from alias
   162  			testTarget = aliasEntry.Link[0]
   163  		}
   164  
   165  		totalTests++
   166  		fmt.Printf("🔍 Testing alias: %s (priority: %d)\n", aliasEntry.Name, aliasEntry.Priority)
   167  
   168  		// Show target being used
   169  		if target != "" {
   170  			fmt.Printf("   🎯 Using override target: %s\n", testTarget)
   171  		} else {
   172  			fmt.Printf("   🎯 Using alias target: %s\n", testTarget)
   173  		}
   174  
   175  		// Test fingerprint detection
   176  		results := testFingerprintDetection(eng, testTarget, aliasEntry, timeout, verbose)
   177  
   178  		if len(results) > 0 {
   179  			successfulMatches++
   180  			fmt.Printf("   ✅ Found %d matching fingerprints:\n", len(results))
   181  			for _, result := range results {
   182  				fmt.Printf("      🎯 %s (%s) - %s\n", result.Name, result.From, result.Version)
   183  			}
   184  		} else {
   185  			fmt.Printf("   ❌ No matching fingerprints detected\n")
   186  		}
   187  		fmt.Println()
   188  	}
   189  
   190  	// Summary
   191  	fmt.Printf("📊 Alias Testing Summary:\n")
   192  	fmt.Printf("   Total aliases tested: %d\n", totalTests)
   193  	fmt.Printf("   Successful matches: %d\n", successfulMatches)
   194  	if totalTests > 0 {
   195  		fmt.Printf("   Success rate: %.1f%%\n", float64(successfulMatches)/float64(totalTests)*100)
   196  	}
   197  }
   198  
   199  func showHelp() {
   200  	fmt.Println("Fingers Alias Tester")
   201  	fmt.Println()
   202  	fmt.Println("Usage:")
   203  	fmt.Println("  test [options]")
   204  	fmt.Println()
   205  	fmt.Println("Options:")
   206  	fmt.Println("  -alias string")
   207  	fmt.Println("        Path to alias YAML file")
   208  	fmt.Println("  -target string")
   209  	fmt.Println("        Target URL or address to test (overrides alias targets if provided)")
   210  	fmt.Println("  -name string")
   211  	fmt.Println("        Filter to test only specific alias by name")
   212  	fmt.Println("  -detect-all")
   213  	fmt.Println("        Run general fingerprint detection on target")
   214  	fmt.Println("  -timeout int")
   215  	fmt.Println("        Request timeout in seconds (default 10)")
   216  	fmt.Println("  -verbose")
   217  	fmt.Println("        Enable verbose output")
   218  	fmt.Println("  -help")
   219  	fmt.Println("        Show this help information")
   220  	fmt.Println()
   221  	fmt.Println("Examples:")
   222  	fmt.Println("  # Run general fingerprint detection")
   223  	fmt.Println("  test -target https://nginx.org -detect-all")
   224  	fmt.Println()
   225  	fmt.Println("  # Test specific alias with override target")
   226  	fmt.Println("  test -alias aliases.yaml -name nginx_test -target https://custom-nginx.com")
   227  	fmt.Println()
   228  	fmt.Println("  # Test all aliases using their defined targets")
   229  	fmt.Println("  test -alias aliases.yaml")
   230  	fmt.Println()
   231  	fmt.Println("  # Test specific alias using its defined target")
   232  	fmt.Println("  test -alias aliases.yaml -name github_test -verbose")
   233  }
   234  
   235  func loadAliasFile(filename string) ([]alias.Alias, error) {
   236  	content, err := ioutil.ReadFile(filename)
   237  	if err != nil {
   238  		return nil, fmt.Errorf("failed to read file: %w", err)
   239  	}
   240  
   241  	var aliases []alias.Alias
   242  	if strings.HasSuffix(filename, ".json") {
   243  		err = json.Unmarshal(content, &aliases)
   244  	} else {
   245  		err = yaml.Unmarshal(content, &aliases)
   246  	}
   247  
   248  	if err != nil {
   249  		return nil, fmt.Errorf("failed to parse file: %w", err)
   250  	}
   251  
   252  	return aliases, nil
   253  }
   254  
   255  func matchesTarget(currentTarget, aliasTarget string) bool {
   256  	// Simple matching logic - can be enhanced
   257  	// Extract domain/IP from URLs for comparison
   258  	currentClean := extractHostFromTarget(currentTarget)
   259  	aliasClean := extractHostFromTarget(aliasTarget)
   260  
   261  	return strings.EqualFold(currentClean, aliasClean) ||
   262  		strings.Contains(strings.ToLower(currentTarget), strings.ToLower(aliasTarget)) ||
   263  		strings.Contains(strings.ToLower(aliasTarget), strings.ToLower(currentTarget))
   264  }
   265  
   266  func extractHostFromTarget(target string) string {
   267  	// Remove protocol if present
   268  	if strings.HasPrefix(target, "http://") {
   269  		target = target[7:]
   270  	} else if strings.HasPrefix(target, "https://") {
   271  		target = target[8:]
   272  	}
   273  
   274  	// Remove path if present
   275  	if idx := strings.Index(target, "/"); idx != -1 {
   276  		target = target[:idx]
   277  	}
   278  
   279  	return target
   280  }
   281  
   282  func testFingerprintDetection(eng *fingers.Engine, target string, aliasEntry alias.Alias, timeout int, verbose bool) []common.Framework {
   283  	// Create HTTP client with timeout
   284  	client := &http.Client{
   285  		Timeout: time.Duration(timeout) * time.Second,
   286  	}
   287  
   288  	// Ensure target has protocol
   289  	if !strings.HasPrefix(target, "http://") && !strings.HasPrefix(target, "https://") {
   290  		target = "http://" + target
   291  	}
   292  
   293  	if verbose {
   294  		fmt.Printf("      🌐 Requesting: %s\n", target)
   295  	}
   296  
   297  	// Make HTTP request
   298  	resp, err := client.Get(target)
   299  	if err != nil {
   300  		if verbose {
   301  			fmt.Printf("      ❌ Request failed: %s\n", err.Error())
   302  		}
   303  		return nil
   304  	}
   305  	defer resp.Body.Close()
   306  
   307  	// Detect frameworks
   308  	frameworks, err := eng.DetectResponse(resp)
   309  	if err != nil {
   310  		if verbose {
   311  			fmt.Printf("      ❌ Detection failed: %s\n", err.Error())
   312  		}
   313  		return nil
   314  	}
   315  
   316  	// Filter results based on alias mappings
   317  	var matchingFrameworks []common.Framework
   318  	for _, framework := range frameworks {
   319  		if isExpectedFramework(*framework, aliasEntry) {
   320  			matchingFrameworks = append(matchingFrameworks, *framework)
   321  		}
   322  	}
   323  
   324  	return matchingFrameworks
   325  }
   326  
   327  func isExpectedFramework(framework common.Framework, aliasEntry alias.Alias) bool {
   328  	// Check if this framework matches any of the expected aliases
   329  	engineName := framework.From.String()
   330  
   331  	if expectedNames, exists := aliasEntry.AliasMap[engineName]; exists {
   332  		for _, expectedName := range expectedNames {
   333  			if strings.EqualFold(framework.Name, expectedName) ||
   334  				strings.Contains(strings.ToLower(framework.Name), strings.ToLower(expectedName)) {
   335  				return true
   336  			}
   337  		}
   338  	}
   339  
   340  	return false
   341  }