mirror of
https://github.com/Vyntral/god-eye.git
synced 2026-08-17 20:17:16 +02:00
feat: Add Multi-Agent AI Orchestration with 8 specialized agents
- Implement 8 specialized AI agents (XSS, SQLi, Auth, API, Crypto, Secrets, Headers, General) - Add fast type-based routing for finding classification - Include OWASP-aligned knowledge bases per agent - Add agent handoff logic for cross-vulnerability detection - Optimize timeouts and parallelism for local LLM - Add new modules: cache, network, fingerprint, secrets, cloud, API, discovery - Update documentation with multi-agent feature
This commit is contained in:
@@ -0,0 +1,698 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// APIFinding represents an API-related discovery
|
||||
type APIFinding struct {
|
||||
Type string `json:"type"` // graphql, rest, swagger, openapi
|
||||
URL string `json:"url"`
|
||||
Method string `json:"method,omitempty"`
|
||||
Issue string `json:"issue,omitempty"` // introspection_enabled, etc.
|
||||
Severity string `json:"severity,omitempty"` // critical, high, medium, low
|
||||
Details map[string]string `json:"details,omitempty"`
|
||||
Endpoints []string `json:"endpoints,omitempty"` // discovered endpoints
|
||||
Version string `json:"version,omitempty"` // API version
|
||||
Auth string `json:"auth,omitempty"` // none, api_key, oauth, etc.
|
||||
}
|
||||
|
||||
// APIScanner discovers and analyzes APIs
|
||||
type APIScanner struct {
|
||||
client *http.Client
|
||||
concurrency int
|
||||
}
|
||||
|
||||
// NewAPIScanner creates a new API scanner
|
||||
func NewAPIScanner(timeout int) *APIScanner {
|
||||
return &APIScanner{
|
||||
client: &http.Client{
|
||||
Timeout: time.Duration(timeout) * time.Second,
|
||||
CheckRedirect: func(req *http.Request, via []*http.Request) error {
|
||||
return http.ErrUseLastResponse
|
||||
},
|
||||
},
|
||||
concurrency: 10,
|
||||
}
|
||||
}
|
||||
|
||||
// ScanHost performs comprehensive API discovery on a host
|
||||
func (as *APIScanner) ScanHost(ctx context.Context, host string) []APIFinding {
|
||||
var findings []APIFinding
|
||||
var mu sync.Mutex
|
||||
var wg sync.WaitGroup
|
||||
|
||||
// Check GraphQL endpoints
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
gqlFindings := as.checkGraphQL(ctx, host)
|
||||
mu.Lock()
|
||||
findings = append(findings, gqlFindings...)
|
||||
mu.Unlock()
|
||||
}()
|
||||
|
||||
// Check Swagger/OpenAPI
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
swaggerFindings := as.checkSwagger(ctx, host)
|
||||
mu.Lock()
|
||||
findings = append(findings, swaggerFindings...)
|
||||
mu.Unlock()
|
||||
}()
|
||||
|
||||
// Check common API paths
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
apiFindings := as.checkCommonAPIPaths(ctx, host)
|
||||
mu.Lock()
|
||||
findings = append(findings, apiFindings...)
|
||||
mu.Unlock()
|
||||
}()
|
||||
|
||||
// Check API versioning issues
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
versionFindings := as.checkAPIVersions(ctx, host)
|
||||
mu.Lock()
|
||||
findings = append(findings, versionFindings...)
|
||||
mu.Unlock()
|
||||
}()
|
||||
|
||||
wg.Wait()
|
||||
return findings
|
||||
}
|
||||
|
||||
// GraphQL introspection query
|
||||
const graphqlIntrospectionQuery = `{"query":"query IntrospectionQuery { __schema { queryType { name } types { name kind description fields { name } } } }"}`
|
||||
|
||||
// checkGraphQL checks for GraphQL endpoints and introspection
|
||||
func (as *APIScanner) checkGraphQL(ctx context.Context, host string) []APIFinding {
|
||||
var findings []APIFinding
|
||||
|
||||
// Common GraphQL paths
|
||||
paths := []string{
|
||||
"/graphql",
|
||||
"/graphiql",
|
||||
"/v1/graphql",
|
||||
"/v2/graphql",
|
||||
"/api/graphql",
|
||||
"/query",
|
||||
"/gql",
|
||||
"/playground",
|
||||
"/console",
|
||||
}
|
||||
|
||||
for _, path := range paths {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return findings
|
||||
default:
|
||||
}
|
||||
|
||||
for _, scheme := range []string{"https", "http"} {
|
||||
url := fmt.Sprintf("%s://%s%s", scheme, host, path)
|
||||
|
||||
// Try POST with introspection query
|
||||
req, err := http.NewRequestWithContext(ctx, "POST", url, strings.NewReader(graphqlIntrospectionQuery))
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("User-Agent", "Mozilla/5.0 (compatible; SecurityScanner/1.0)")
|
||||
|
||||
resp, err := as.client.Do(req)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
body, _ := io.ReadAll(io.LimitReader(resp.Body, 100*1024))
|
||||
resp.Body.Close()
|
||||
|
||||
// Check if introspection is enabled
|
||||
if resp.StatusCode == 200 && strings.Contains(string(body), "__schema") {
|
||||
finding := APIFinding{
|
||||
Type: "graphql",
|
||||
URL: url,
|
||||
Method: "POST",
|
||||
Issue: "introspection_enabled",
|
||||
Severity: "high",
|
||||
Details: map[string]string{
|
||||
"description": "GraphQL introspection is enabled, exposing schema",
|
||||
},
|
||||
}
|
||||
|
||||
// Extract type names
|
||||
var gqlResp map[string]interface{}
|
||||
if json.Unmarshal(body, &gqlResp) == nil {
|
||||
if data, ok := gqlResp["data"].(map[string]interface{}); ok {
|
||||
if schema, ok := data["__schema"].(map[string]interface{}); ok {
|
||||
if types, ok := schema["types"].([]interface{}); ok {
|
||||
for i, t := range types {
|
||||
if i >= 20 {
|
||||
break
|
||||
}
|
||||
if typeMap, ok := t.(map[string]interface{}); ok {
|
||||
if name, ok := typeMap["name"].(string); ok {
|
||||
if !strings.HasPrefix(name, "__") {
|
||||
finding.Endpoints = append(finding.Endpoints, name)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
findings = append(findings, finding)
|
||||
break // Found on this scheme, don't check http if https worked
|
||||
}
|
||||
|
||||
// Check for GraphQL endpoint without introspection
|
||||
if resp.StatusCode == 200 || resp.StatusCode == 400 {
|
||||
contentType := resp.Header.Get("Content-Type")
|
||||
if strings.Contains(contentType, "json") || strings.Contains(string(body), "errors") {
|
||||
findings = append(findings, APIFinding{
|
||||
Type: "graphql",
|
||||
URL: url,
|
||||
Method: "POST",
|
||||
Issue: "endpoint_found",
|
||||
Severity: "info",
|
||||
Details: map[string]string{
|
||||
"introspection": "disabled",
|
||||
},
|
||||
})
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return findings
|
||||
}
|
||||
|
||||
// checkSwagger checks for Swagger/OpenAPI documentation with proper JSON validation
|
||||
func (as *APIScanner) checkSwagger(ctx context.Context, host string) []APIFinding {
|
||||
var findings []APIFinding
|
||||
|
||||
// Common Swagger/OpenAPI paths
|
||||
paths := []string{
|
||||
"/swagger.json",
|
||||
"/swagger.yaml",
|
||||
"/swagger/v1/swagger.json",
|
||||
"/api/swagger.json",
|
||||
"/openapi.json",
|
||||
"/openapi.yaml",
|
||||
"/api-docs",
|
||||
"/api-docs.json",
|
||||
"/v1/api-docs",
|
||||
"/v2/api-docs",
|
||||
"/v3/api-docs",
|
||||
"/docs/api",
|
||||
"/swagger-ui.html",
|
||||
"/swagger-ui/",
|
||||
"/swagger/",
|
||||
"/api/docs",
|
||||
"/api/v1/docs",
|
||||
"/.well-known/openapi.json",
|
||||
"/redoc",
|
||||
}
|
||||
|
||||
for _, path := range paths {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return findings
|
||||
default:
|
||||
}
|
||||
|
||||
for _, scheme := range []string{"https", "http"} {
|
||||
url := fmt.Sprintf("%s://%s%s", scheme, host, path)
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
req.Header.Set("User-Agent", "Mozilla/5.0 (compatible; SecurityScanner/1.0)")
|
||||
|
||||
resp, err := as.client.Do(req)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
body, _ := io.ReadAll(io.LimitReader(resp.Body, 500*1024))
|
||||
resp.Body.Close()
|
||||
|
||||
if resp.StatusCode == 200 {
|
||||
contentType := resp.Header.Get("Content-Type")
|
||||
bodyStr := string(body)
|
||||
|
||||
// IMPROVED: Validate actual Swagger/OpenAPI JSON structure, not just strings
|
||||
isValidSwagger, swaggerVersion := validateSwaggerStructure(body)
|
||||
|
||||
// Check for Swagger UI (HTML page)
|
||||
isSwaggerUI := strings.Contains(bodyStr, "swagger-ui") &&
|
||||
strings.Contains(contentType, "text/html")
|
||||
|
||||
if isValidSwagger {
|
||||
finding := APIFinding{
|
||||
Type: "swagger",
|
||||
URL: url,
|
||||
Method: "GET",
|
||||
Severity: "medium",
|
||||
Issue: "api_documentation_exposed",
|
||||
Details: map[string]string{
|
||||
"description": "API documentation is publicly accessible",
|
||||
"swagger_version": swaggerVersion,
|
||||
"confidence": "high",
|
||||
},
|
||||
}
|
||||
|
||||
// Extract endpoints from swagger
|
||||
finding.Endpoints = extractSwaggerEndpoints(body)
|
||||
finding.Version = swaggerVersion
|
||||
|
||||
findings = append(findings, finding)
|
||||
break
|
||||
} else if isSwaggerUI {
|
||||
// Swagger UI is still useful to report, but with lower confidence
|
||||
findings = append(findings, APIFinding{
|
||||
Type: "swagger",
|
||||
URL: url,
|
||||
Method: "GET",
|
||||
Severity: "low",
|
||||
Issue: "swagger_ui_exposed",
|
||||
Details: map[string]string{
|
||||
"description": "Swagger UI page detected",
|
||||
"confidence": "medium",
|
||||
},
|
||||
})
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return findings
|
||||
}
|
||||
|
||||
// validateSwaggerStructure validates actual Swagger/OpenAPI JSON structure
|
||||
// Returns (isValid, version) - reduces false positives by checking real structure
|
||||
func validateSwaggerStructure(body []byte) (bool, string) {
|
||||
var doc map[string]interface{}
|
||||
if err := json.Unmarshal(body, &doc); err != nil {
|
||||
return false, ""
|
||||
}
|
||||
|
||||
// Check for OpenAPI 3.x format
|
||||
if openapi, ok := doc["openapi"].(string); ok {
|
||||
if strings.HasPrefix(openapi, "3.") {
|
||||
// Validate required OpenAPI 3.x fields
|
||||
if _, hasInfo := doc["info"].(map[string]interface{}); hasInfo {
|
||||
if _, hasPaths := doc["paths"].(map[string]interface{}); hasPaths {
|
||||
return true, openapi
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check for Swagger 2.0 format
|
||||
if swagger, ok := doc["swagger"].(string); ok {
|
||||
if swagger == "2.0" {
|
||||
// Validate required Swagger 2.0 fields
|
||||
if _, hasInfo := doc["info"].(map[string]interface{}); hasInfo {
|
||||
if _, hasPaths := doc["paths"].(map[string]interface{}); hasPaths {
|
||||
return true, "2.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check for minimal valid structure (paths with actual endpoints)
|
||||
if paths, ok := doc["paths"].(map[string]interface{}); ok {
|
||||
if len(paths) > 0 {
|
||||
// Verify at least one path has HTTP methods
|
||||
for _, pathDef := range paths {
|
||||
if pathObj, ok := pathDef.(map[string]interface{}); ok {
|
||||
for method := range pathObj {
|
||||
if isHTTPMethod(method) {
|
||||
return true, "unknown"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false, ""
|
||||
}
|
||||
|
||||
// isHTTPMethod checks if a string is a valid HTTP method
|
||||
func isHTTPMethod(s string) bool {
|
||||
methods := []string{"get", "post", "put", "delete", "patch", "options", "head"}
|
||||
lower := strings.ToLower(s)
|
||||
for _, m := range methods {
|
||||
if lower == m {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// checkCommonAPIPaths checks for common API endpoints with improved false positive filtering
|
||||
func (as *APIScanner) checkCommonAPIPaths(ctx context.Context, host string) []APIFinding {
|
||||
var findings []APIFinding
|
||||
|
||||
// First, get baseline responses to detect WAF/global auth
|
||||
baselineStatus := as.getBaselineResponse(ctx, host)
|
||||
|
||||
// Sensitive API paths
|
||||
sensitivePaths := map[string]string{
|
||||
"/api/users": "User enumeration possible",
|
||||
"/api/v1/users": "User enumeration possible",
|
||||
"/api/admin": "Admin API exposed",
|
||||
"/api/v1/admin": "Admin API exposed",
|
||||
"/api/config": "Configuration endpoint exposed",
|
||||
"/api/settings": "Settings endpoint exposed",
|
||||
"/api/debug": "Debug endpoint exposed",
|
||||
"/api/health": "Health check exposed",
|
||||
"/api/status": "Status endpoint exposed",
|
||||
"/api/metrics": "Metrics endpoint exposed",
|
||||
"/api/internal": "Internal API exposed",
|
||||
"/api/private": "Private API exposed",
|
||||
"/actuator": "Spring Boot Actuator exposed",
|
||||
"/actuator/env": "Environment variables exposed",
|
||||
"/actuator/heapdump": "Heap dump endpoint exposed",
|
||||
"/actuator/mappings": "API mappings exposed",
|
||||
"/metrics": "Prometheus metrics exposed",
|
||||
"/debug/pprof": "Go pprof exposed",
|
||||
"/debug/vars": "Debug vars exposed",
|
||||
"/__debug__": "Debug mode enabled",
|
||||
"/api/v1/internal": "Internal API exposed",
|
||||
"/api/keys": "API keys endpoint exposed",
|
||||
"/api/tokens": "Tokens endpoint exposed",
|
||||
}
|
||||
|
||||
for path, description := range sensitivePaths {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return findings
|
||||
default:
|
||||
}
|
||||
|
||||
for _, scheme := range []string{"https", "http"} {
|
||||
url := fmt.Sprintf("%s://%s%s", scheme, host, path)
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
req.Header.Set("User-Agent", "Mozilla/5.0 (compatible; SecurityScanner/1.0)")
|
||||
|
||||
resp, err := as.client.Do(req)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
body, _ := io.ReadAll(io.LimitReader(resp.Body, 10*1024))
|
||||
resp.Body.Close()
|
||||
|
||||
// IMPROVED: Skip if this is likely a WAF/global response
|
||||
if resp.StatusCode == 401 || resp.StatusCode == 403 {
|
||||
// If baseline returns same status, this is likely WAF/global auth, not endpoint-specific
|
||||
if baselineStatus == resp.StatusCode {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// Found if not 404 and not generic error
|
||||
if resp.StatusCode == 200 || resp.StatusCode == 401 || resp.StatusCode == 403 {
|
||||
// IMPROVED: Validate the response looks like an actual API response
|
||||
confidence := validateAPIResponse(body, resp.Header.Get("Content-Type"), resp.StatusCode)
|
||||
if confidence == "none" {
|
||||
continue
|
||||
}
|
||||
|
||||
severity := "low"
|
||||
if strings.Contains(path, "admin") || strings.Contains(path, "internal") ||
|
||||
strings.Contains(path, "debug") || strings.Contains(path, "actuator") {
|
||||
severity = "high"
|
||||
} else if strings.Contains(path, "users") || strings.Contains(path, "config") {
|
||||
severity = "medium"
|
||||
}
|
||||
|
||||
// Lower severity for protected endpoints
|
||||
if resp.StatusCode == 401 || resp.StatusCode == 403 {
|
||||
if severity == "high" {
|
||||
severity = "medium"
|
||||
} else {
|
||||
severity = "low"
|
||||
}
|
||||
}
|
||||
|
||||
auth := "none"
|
||||
if resp.StatusCode == 401 {
|
||||
auth = "required"
|
||||
} else if resp.StatusCode == 403 {
|
||||
auth = "forbidden"
|
||||
}
|
||||
|
||||
findings = append(findings, APIFinding{
|
||||
Type: "rest",
|
||||
URL: url,
|
||||
Method: "GET",
|
||||
Issue: "sensitive_endpoint",
|
||||
Severity: severity,
|
||||
Auth: auth,
|
||||
Details: map[string]string{
|
||||
"description": description,
|
||||
"status_code": fmt.Sprintf("%d", resp.StatusCode),
|
||||
"confidence": confidence,
|
||||
},
|
||||
})
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return findings
|
||||
}
|
||||
|
||||
// getBaselineResponse gets the response for a random non-existent path
|
||||
// to detect global WAF/auth that returns same response for all paths
|
||||
func (as *APIScanner) getBaselineResponse(ctx context.Context, host string) int {
|
||||
// Use a random path that shouldn't exist
|
||||
url := fmt.Sprintf("https://%s/___baseline_test_path_12345___", host)
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
req.Header.Set("User-Agent", "Mozilla/5.0 (compatible; SecurityScanner/1.0)")
|
||||
|
||||
resp, err := as.client.Do(req)
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
resp.Body.Close()
|
||||
|
||||
return resp.StatusCode
|
||||
}
|
||||
|
||||
// validateAPIResponse checks if a response looks like a real API response
|
||||
// Returns confidence level: "high", "medium", "low", "none"
|
||||
func validateAPIResponse(body []byte, contentType string, statusCode int) string {
|
||||
bodyStr := string(body)
|
||||
|
||||
// If it's a generic HTML error page, it's likely not a real API endpoint
|
||||
if strings.Contains(contentType, "text/html") {
|
||||
// Check for common error page patterns
|
||||
if strings.Contains(bodyStr, "<!DOCTYPE") || strings.Contains(bodyStr, "<html") {
|
||||
// HTML pages for API endpoints are suspicious
|
||||
// Unless it's a documentation or login page
|
||||
if strings.Contains(bodyStr, "login") || strings.Contains(bodyStr, "sign in") {
|
||||
return "medium"
|
||||
}
|
||||
return "none"
|
||||
}
|
||||
}
|
||||
|
||||
// JSON responses are good indicators of real API endpoints
|
||||
if strings.Contains(contentType, "application/json") {
|
||||
var js interface{}
|
||||
if json.Unmarshal(body, &js) == nil {
|
||||
return "high"
|
||||
}
|
||||
}
|
||||
|
||||
// For 200 status, we expect some content
|
||||
if statusCode == 200 {
|
||||
if len(body) == 0 {
|
||||
return "low"
|
||||
}
|
||||
// Check for JSON-like structure
|
||||
if (bodyStr[0] == '{' || bodyStr[0] == '[') {
|
||||
return "high"
|
||||
}
|
||||
return "medium"
|
||||
}
|
||||
|
||||
// For 401/403, check for API-style error messages
|
||||
if statusCode == 401 || statusCode == 403 {
|
||||
// Common API error patterns
|
||||
if strings.Contains(bodyStr, "unauthorized") ||
|
||||
strings.Contains(bodyStr, "forbidden") ||
|
||||
strings.Contains(bodyStr, "\"error\"") ||
|
||||
strings.Contains(bodyStr, "\"message\"") {
|
||||
return "high"
|
||||
}
|
||||
// Short responses are likely API responses
|
||||
if len(body) < 500 {
|
||||
return "medium"
|
||||
}
|
||||
return "low"
|
||||
}
|
||||
|
||||
return "medium"
|
||||
}
|
||||
|
||||
// checkAPIVersions checks for deprecated/old API versions
|
||||
func (as *APIScanner) checkAPIVersions(ctx context.Context, host string) []APIFinding {
|
||||
var findings []APIFinding
|
||||
|
||||
// API version paths to check
|
||||
versions := []string{
|
||||
"/api/v0/",
|
||||
"/api/v1/",
|
||||
"/api/v2/",
|
||||
"/api/v3/",
|
||||
"/v0/",
|
||||
"/v1/",
|
||||
"/v2/",
|
||||
"/v3/",
|
||||
}
|
||||
|
||||
foundVersions := []string{}
|
||||
|
||||
for _, version := range versions {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return findings
|
||||
default:
|
||||
}
|
||||
|
||||
url := fmt.Sprintf("https://%s%s", host, version)
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
req.Header.Set("User-Agent", "Mozilla/5.0 (compatible; SecurityScanner/1.0)")
|
||||
|
||||
resp, err := as.client.Do(req)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != 404 && resp.StatusCode != 0 {
|
||||
foundVersions = append(foundVersions, version)
|
||||
}
|
||||
}
|
||||
|
||||
// If multiple versions found, report potential issue
|
||||
if len(foundVersions) > 1 {
|
||||
findings = append(findings, APIFinding{
|
||||
Type: "rest",
|
||||
URL: fmt.Sprintf("https://%s", host),
|
||||
Issue: "multiple_api_versions",
|
||||
Severity: "low",
|
||||
Details: map[string]string{
|
||||
"description": "Multiple API versions detected, old versions may be deprecated",
|
||||
},
|
||||
Endpoints: foundVersions,
|
||||
})
|
||||
}
|
||||
|
||||
// Check for v0 specifically (often test/dev)
|
||||
for _, v := range foundVersions {
|
||||
if strings.Contains(v, "v0") {
|
||||
findings = append(findings, APIFinding{
|
||||
Type: "rest",
|
||||
URL: fmt.Sprintf("https://%s%s", host, v),
|
||||
Issue: "dev_api_version",
|
||||
Severity: "medium",
|
||||
Details: map[string]string{
|
||||
"description": "Version 0 API found, may be development/test version",
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return findings
|
||||
}
|
||||
|
||||
// extractSwaggerEndpoints extracts API paths from swagger JSON
|
||||
func extractSwaggerEndpoints(body []byte) []string {
|
||||
var endpoints []string
|
||||
seen := make(map[string]bool)
|
||||
|
||||
// Try to parse as JSON
|
||||
var swagger map[string]interface{}
|
||||
if err := json.Unmarshal(body, &swagger); err != nil {
|
||||
return endpoints
|
||||
}
|
||||
|
||||
// Extract from "paths" object
|
||||
if paths, ok := swagger["paths"].(map[string]interface{}); ok {
|
||||
for path := range paths {
|
||||
if !seen[path] {
|
||||
seen[path] = true
|
||||
endpoints = append(endpoints, path)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Limit to 50 endpoints
|
||||
if len(endpoints) > 50 {
|
||||
endpoints = endpoints[:50]
|
||||
}
|
||||
|
||||
return endpoints
|
||||
}
|
||||
|
||||
// ExtractAPIURLs extracts API-related URLs from content
|
||||
func ExtractAPIURLs(content string) []string {
|
||||
var urls []string
|
||||
seen := make(map[string]bool)
|
||||
|
||||
patterns := []*regexp.Regexp{
|
||||
regexp.MustCompile(`["'](/api/[^"'\s]+)["']`),
|
||||
regexp.MustCompile(`["'](/v\d+/[^"'\s]+)["']`),
|
||||
regexp.MustCompile(`["'](https?://[^"'\s]+/api/[^"'\s]+)["']`),
|
||||
regexp.MustCompile(`["'](https?://api\.[^"'\s]+)["']`),
|
||||
}
|
||||
|
||||
for _, pattern := range patterns {
|
||||
matches := pattern.FindAllStringSubmatch(content, -1)
|
||||
for _, match := range matches {
|
||||
if len(match) > 1 && !seen[match[1]] {
|
||||
seen[match[1]] = true
|
||||
urls = append(urls, match[1])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return urls
|
||||
}
|
||||
Reference in New Issue
Block a user