package scanner import ( "context" "fmt" "os" "os/exec" "path/filepath" "strings" "time" ) const ( gotestwafTimeout = 120 * time.Second targetURL = "https://git.sechpoint.app" ) // RunGoTestWAF executes GoTestWAF scan against the target server. // The report is saved to reports/ directory with the given reportName. // Returns the raw output for AI analysis. func RunGoTestWAF(ctx context.Context, projectRoot string, reportName string) (string, error) { binaryPath := filepath.Join(projectRoot, "gotestwaf") // Verify binary exists if _, err := os.Stat(binaryPath); os.IsNotExist(err) { return "", fmt.Errorf("GoTestWAF binary not found at %s", binaryPath) } reportsDir := filepath.Join(projectRoot, "reports") if err := os.MkdirAll(reportsDir, 0755); err != nil { return "", fmt.Errorf("failed to create reports directory: %w", err) } // Create context with timeout scanCtx, cancel := context.WithTimeout(ctx, gotestwafTimeout) defer cancel() cmd := exec.CommandContext(scanCtx, binaryPath, "--url", targetURL, "--configPath", "gotestwaf-config.yaml", "--testCasesPath", "testcases", "--maxIdleConns", "2", "--maxRedirects", "10", "--reportPath", reportsDir, "--reportName", reportName, "--reportFormat", "html", "--wafName", "generic", "--skipWAFBlockCheck", "--nonBlockedAsPassed", "--tlsVerify", "--noEmailReport", "--quiet", ) cmd.Dir = projectRoot output, err := cmd.CombinedOutput() outputStr := string(output) if err != nil { if scanCtx.Err() == context.DeadlineExceeded { return outputStr, fmt.Errorf("GoTestWAF scan timed out after %v", gotestwafTimeout) } return outputStr, fmt.Errorf("GoTestWAF scan failed: %w", err) } return outputStr, nil } // FindMostCriticalSubdomain selects the most security-critical subdomain // from a list of discovered subdomains. Prioritizes: // 1. Subdomains with "api", "admin", "dashboard" in name // 2. Subdomains vs root domain // 3. First subdomain alphabetically as tiebreaker func FindMostCriticalSubdomain(subdomains []string, originalDomain string) string { if len(subdomains) == 0 { return originalDomain } // Score each subdomain for "criticality" type scored struct { domain string score int } var candidates []scored for _, s := range subdomains { score := 0 lower := strings.ToLower(s) if strings.Contains(lower, "api") { score += 10 } if strings.Contains(lower, "admin") { score += 8 } if strings.Contains(lower, "dashboard") { score += 8 } if strings.Contains(lower, "gateway") { score += 6 } if strings.Contains(lower, "auth") { score += 6 } if strings.Contains(lower, "console") { score += 5 } if strings.Contains(lower, "dev") || strings.Contains(lower, "staging") { score += 4 } if strings.Contains(lower, "app") { score += 2 } // Prefer subdomains over root if strings.HasPrefix(s, "www.") { score += 1 } if s != originalDomain && !strings.HasPrefix(s, "www.") { score += 3 } candidates = append(candidates, scored{s, score}) } // Find highest score best := candidates[0] for _, c := range candidates[1:] { if c.score > best.score || (c.score == best.score && c.domain < best.domain) { best = c } } return best.domain }