--- name: detecting-shadow-api-endpoints description: Discover and inventory shadow API endpoints that operate outside documented specifications using traffic analysis, code scanning, and API discovery platforms. domain: cybersecurity subdomain: api-security tags: - api-security - shadow-apis - api-discovery - undocumented-apis - zombie-apis - api-inventory - attack-surface-management - api-governance version: '1.0' author: mahipal license: Apache-2.0 nist_csf: - PR.PS-01 - ID.RA-01 - PR.DS-10 - DE.CM-01 --- # Detecting Shadow API Endpoints ## Overview Shadow APIs are API endpoints operating within an organization's environment that are not tracked, documented, or secured. They emerge from rapid development cycles, forgotten test environments, deprecated API versions left running, third-party integrations, or developer side projects deployed without governance. Shadow APIs bypass authentication and monitoring controls, creating hidden entry points for attackers. Studies show that up to 30% of API endpoints in large organizations are undocumented, making shadow API detection a critical component of API security posture management. ## When to Use - When investigating security incidents that require detecting shadow api endpoints - When building detection rules or threat hunting queries for this domain - When SOC analysts need structured procedures for this analysis type - When validating security monitoring coverage for related attack techniques ## Detection Gaps & Validation - **Spec-normalization blind spots:** path-templating mismatches (`/users/{id}` vs observed `/users/{uuid}`) make documented endpoints look undocumented (false positive) or hide real shadows; verify your PARAM_PATTERNS cover hashids, ULIDs, and multi-segment IDs. - **Non-/api prefixes missed:** the log filter `startswith('/api')`/`/v` drops shadow endpoints under `/internal`, `/graphql`, `/_next`, `/actuator`, host-based vhosts, and non-standard ports - widen capture before concluding "no shadows." - **Zombie/deprecated versions:** `v0`, `v1` left running after `v2` launch rarely appear in current traffic; enumerate from source/CI and cloud (Lambda URLs, ALB rules) too, not just logs. - **Low-traffic shadows stay invisible:** sampling or short capture windows miss endpoints hit a few times a day - correlate over a long window. **How to validate the detection fires:** stand up a deliberately undocumented test route, send traffic, and confirm it surfaces as shadow with correct risk scoring. **Tune false positives** by reconciling against every OpenAPI/Swagger source (not one file) and excluding health/metrics probes that are known-and-accepted but undocumented. ## Prerequisites - API gateway or reverse proxy with traffic logging (Kong, AWS API Gateway, Envoy) - Network traffic capture capability (packet broker, port mirroring) - Access to source code repositories and CI/CD pipeline configurations - Cloud provider access for configuration scanning (AWS, GCP, Azure) - API documentation inventory (OpenAPI specs, Swagger docs) - Python 3.8+ for custom discovery tooling ## Detection Methods ### 1. Traffic Analysis and Comparison Compare live API traffic against documented OpenAPI specifications to identify undocumented endpoints: ```python #!/usr/bin/env python3 """Shadow API Endpoint Detector Compares observed API traffic patterns against documented OpenAPI specifications to identify undocumented (shadow) endpoints. """ import json import re import yaml import sys from collections import defaultdict from datetime import datetime from typing import Dict, List, Set, Tuple, Optional from dataclasses import dataclass, field @dataclass class DiscoveredEndpoint: method: str path_pattern: str first_seen: str last_seen: str request_count: int source_ips: Set[str] = field(default_factory=set) status_codes: Set[int] = field(default_factory=set) has_auth_header: bool = False documented: bool = False class ShadowAPIDetector: # Common patterns for parameterized path segments PARAM_PATTERNS = [ (re.compile(r'/\d+'), '/{id}'), (re.compile(r'/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}'), '/{uuid}'), (re.compile(r'/[a-zA-Z0-9]{20,40}'), '/{token}'), ] def __init__(self): self.documented_endpoints: Set[Tuple[str, str]] = set() self.discovered_endpoints: Dict[Tuple[str, str], DiscoveredEndpoint] = {} def load_openapi_spec(self, spec_path: str): """Load documented endpoints from OpenAPI specification.""" with open(spec_path, 'r') as f: if spec_path.endswith('.json'): spec = json.load(f) else: spec = yaml.safe_load(f) paths = spec.get('paths', {}) for path, methods in paths.items(): # Normalize OpenAPI path parameters normalized_path = re.sub(r'\{[^}]+\}', '{id}', path) for method in methods: if method.upper() in ('GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS'): self.documented_endpoints.add((method.upper(), normalized_path)) print(f"Loaded {len(self.documented_endpoints)} documented endpoints from {spec_path}") def normalize_path(self, path: str) -> str: """Normalize an observed path by replacing dynamic segments with placeholders.""" # Remove query string path = path.split('?')[0] for pattern, replacement in self.PARAM_PATTERNS: path = pattern.sub(replacement, path) return path def process_access_log(self, log_file: str, log_format: str = "common"): """Process API access logs to discover endpoints.""" patterns = { "common": re.compile( r'(?P[\d.]+)\s+\S+\s+\S+\s+\[(?P