# Performing API Inventory And Discovery

> 执行 API 资产清点和发现，以识别组织环境中的所有 API 端点，包括已记录的、 未记录的、影子 API、僵尸 API 和已废弃的 API。测试人员通过被动流量分析、 主动扫描、DNS 枚举、JavaScript 分析和云资源清点来构建全面的 API 目录。 映射至 OWASP API9:2023 不当资产管理。当请求涉及 API 发现、影子 API 检测、 API 清单审计或攻击面测绘时触发。

- Skill: `killvxk/performing-api-inventory-and-discovery` (Agent Skill, multi-file: 4 files)
- Install (CLI): `npx skillmds@latest add killvxk/performing-api-inventory-and-discovery`
- Raw SKILL.md: https://api.skillmd.com/api/skills/killvxk/performing-api-inventory-and-discovery/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Product & Planning
- License: Apache-2.0
- Author: killvxk (https://skillmd.com/u/killvxk)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/killvxk/performing-api-inventory-and-discovery

---

# 执行 API 资产清点与发现

## 适用场景

- 在安全评估前测绘组织的完整 API 攻击面
- 识别由开发团队未经安全审查部署的影子 API（Shadow API）
- 发现仍可访问但无人维护的已废弃或僵尸 API 版本
- 发现通过移动应用、SPA 或微服务暴露的未记录 API 端点
- 为合规需求（PCI-DSS、SOC2、GDPR）建立 API 清单

**不适用于** 未经书面授权的场景。API 发现涉及扫描网络基础设施和分析流量。

## 前置条件

- 明确授权文件，指定目标域名和网络范围
- 被动流量捕获能力（网络分路器、代理或云流量镜像）
- 主动扫描工具：Amass、subfinder、httpx 和 nuclei
- JavaScript 分析工具：LinkFinder、JS-Miner 或自定义解析器
- 访问云控制台（AWS、Azure、GCP）进行 API 网关清点
- Burp Suite Professional 用于被动 API 端点发现

## 工作流程

### 步骤 1：通过流量分析进行被动 API 发现

```python
import re
import json
from collections import defaultdict

# 从浏览器开发者工具或代理解析 HAR 文件
def analyze_har_for_apis(har_file_path):
    """从 HTTP 归档（HAR）文件中提取 API 端点。"""
    with open(har_file_path) as f:
        har = json.load(f)

    api_endpoints = defaultdict(lambda: {
        "methods": set(), "content_types": set(),
        "auth_types": set(), "count": 0
    })

    for entry in har["log"]["entries"]:
        url = entry["request"]["url"]
        method = entry["request"]["method"]

        # 识别 API 模式
        api_patterns = [
            r'/api/', r'/v\d+/', r'/graphql', r'/rest/',
            r'/ws/', r'/rpc/', r'/grpc', r'/json',
        ]

        if any(re.search(p, url) for p in api_patterns):
            # 规范化 URL（删除查询参数和 ID）
            normalized = re.sub(r'\?.*$', '', url)
            normalized = re.sub(r'/\d+(/|$)', '/{id}\\1', normalized)
            normalized = re.sub(
                r'/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}',
                '/{uuid}', normalized)

            ep = api_endpoints[normalized]
            ep["methods"].add(method)
            ep["count"] += 1

            # 检测认证类型
            for header in entry["request"]["headers"]:
                name = header["name"].lower()
                if name == "authorization":
                    if "bearer" in header["value"].lower():
                        ep["auth_types"].add("Bearer/JWT")
                    elif "basic" in header["value"].lower():
                        ep["auth_types"].add("Basic")
                elif name == "x-api-key":
                    ep["auth_types"].add("API Key")

            # 检测内容类型
            content_type = next(
                (h["value"] for h in entry["request"]["headers"]
                 if h["name"].lower() == "content-type"), None)
            if content_type:
                ep["content_types"].add(content_type.split(";")[0])

    print(f"发现 {len(api_endpoints)} 个唯一 API 端点：\n")
    for url, info in sorted(api_endpoints.items()):
        methods = ", ".join(sorted(info["methods"]))
        auth = ", ".join(info["auth_types"]) or "无"
        print(f"  [{methods}] {url}")
        print(f"    认证：{auth} | 请求次数：{info['count']}")

    return api_endpoints
```

### 步骤 2：主动 API 端点发现

```bash
# DNS 枚举发现 API 子域名
amass enum -d example.com -o amass_results.txt
subfinder -d example.com -o subfinder_results.txt

# 过滤与 API 相关的子域名
grep -iE '(api|rest|graphql|ws|gateway|backend|internal|staging|dev|v1|v2)' \
    amass_results.txt subfinder_results.txt | sort -u > api_subdomains.txt

# 检查哪些子域名处于活跃状态
cat api_subdomains.txt | httpx -status-code -content-length -title \
    -tech-detect -o live_apis.txt

# 在每个活跃子域名上探测常见 API 路径
cat api_subdomains.txt | while read domain; do
    for path in /api /api/v1 /api/v2 /graphql /swagger.json /openapi.json \
                /api-docs /docs /health /status /metrics /actuator; do
        curl -s -o /dev/null -w "%{http_code} %{url_effective}\n" \
            "https://${domain}${path}" 2>/dev/null | grep -v "^404"
    done
done
```

```python
import requests
import concurrent.futures

def discover_api_endpoints(base_domains):
    """主动探测已发现域名上的 API 端点。"""

    # 待测试的常见 API 路径
    API_PATHS = [
        "/api", "/api/v1", "/api/v2", "/api/v3",
        "/graphql", "/gql", "/query",
        "/rest", "/json", "/rpc",
        "/swagger.json", "/swagger/v1/swagger.json",
        "/openapi.json", "/openapi.yaml", "/api-docs",
        "/docs", "/redoc", "/explorer",
        "/.well-known/openid-configuration",
        "/health", "/healthz", "/ready",
        "/status", "/info", "/version",
        "/metrics", "/prometheus",
        "/actuator", "/actuator/health", "/actuator/info",
        "/admin", "/admin/api", "/internal",
        "/debug", "/debug/vars", "/debug/pprof",
        "/ws", "/websocket", "/socket.io",
        "/grpc", "/twirp",
    ]

    discovered = []

    def check_endpoint(domain, path):
        for scheme in ["https", "http"]:
            url = f"{scheme}://{domain}{path}"
            try:
                resp = requests.get(url, timeout=5, allow_redirects=False,
                                  verify=False)
                if resp.status_code not in (404, 502, 503):
                    return {
                        "url": url,
                        "status": resp.status_code,
                        "content_type": resp.headers.get("Content-Type", ""),
                        "server": resp.headers.get("Server", ""),
                        "size": len(resp.content),
                    }
            except requests.exceptions.RequestException:
                pass
        return None

    with concurrent.futures.ThreadPoolExecutor(max_workers=20) as executor:
        futures = {}
        for domain in base_domains:
            for path in API_PATHS:
                future = executor.submit(check_endpoint, domain, path)
                futures[future] = (domain, path)

        for future in concurrent.futures.as_completed(futures):
            result = future.result()
            if result:
                discovered.append(result)
                print(f"  [发现] {result['url']} -> {result['status']} ({result['content_type']})")

    return discovered
```

### 步骤 3：从 JavaScript 源码分析 API 端点

```python
import re
import requests

def extract_apis_from_javascript(js_urls):
    """从 JavaScript 源文件中提取 API 端点。"""
    api_pattern = re.compile(
        r'''(?:['"`])((?:/api/|/v[0-9]+/|/graphql|/rest/)[^'"`\s<>{}]+)(?:['"`])''',
        re.IGNORECASE
    )
    url_pattern = re.compile(
        r'''(?:['"`])(https?://[a-zA-Z0-9._-]+(?:\.[a-zA-Z]{2,})+(?:/[^'"`\s<>{}]*)?)(?:['"`])'''
    )
    fetch_pattern = re.compile(
        r'''(?:fetch|axios|ajax|XMLHttpRequest|\.get|\.post|\.put|\.delete|\.patch)\s*\(\s*(?:['"`])([^'"`]+)'''
    )

    all_endpoints = set()

    for js_url in js_urls:
        try:
            resp = requests.get(js_url, timeout=10)
            content = resp.text

            # 提取相对 API 路径
            for match in api_pattern.findall(content):
                all_endpoints.add(("relative", match))

            # 提取绝对 URL
            for match in url_pattern.findall(content):
                if any(kw in match.lower() for kw in ["/api", "/v1", "/v2", "graphql"]):
                    all_endpoints.add(("absolute", match))

            # 从 fetch/axios 调用中提取
            for match in fetch_pattern.findall(content):
                all_endpoints.add(("fetch", match))

        except requests.exceptions.RequestException:
            pass

    print(f"\n从 JavaScript 中发现的 API 端点（{len(all_endpoints)} 个）：")
    for source, endpoint in sorted(all_endpoints):
        print(f"  [{source}] {endpoint}")

    return all_endpoints

# 从目标域名中查找 JavaScript 文件
def find_js_files(domain):
    """从 Web 应用中发现 JavaScript 文件。"""
    resp = requests.get(f"https://{domain}", timeout=10)
    js_files = re.findall(r'src=["']([^"']+\.js[^"']*)', resp.text)
    full_urls = []
    for js in js_files:
        if js.startswith("http"):
            full_urls.append(js)
        elif js.startswith("//"):
            full_urls.append(f"https:{js}")
        elif js.startswith("/"):
            full_urls.append(f"https://{domain}{js}")
    return full_urls
```

### 步骤 4：云 API 网关清点

```python
import boto3

def inventory_aws_apis():
    """清点 AWS API Gateway 中的所有 API。"""
    apigw = boto3.client('apigateway')
    apigwv2 = boto3.client('apigatewayv2')

    apis = []

    # REST API（API Gateway v1）
    rest_apis = apigw.get_rest_apis()
    for api in rest_apis['items']:
        resources = apigw.get_resources(restApiId=api['id'])
        stages = apigw.get_stages(restApiId=api['id'])

        for stage in stages['item']:
            for resource in resources['items']:
                for method in resource.get('resourceMethods', {}).keys():
                    apis.append({
                        "type": "REST",
                        "name": api['name'],
                        "stage": stage['stageName'],
                        "path": resource['path'],
                        "method": method,
                        "url": f"https://{api['id']}.execute-api.{boto3.session.Session().region_name}.amazonaws.com/{stage['stageName']}{resource['path']}",
                        "created": str(api.get('createdDate', '')),
                    })

    # HTTP API（API Gateway v2）
    http_apis = apigwv2.get_apis()
    for api in http_apis['Items']:
        routes = apigwv2.get_routes(ApiId=api['ApiId'])
        stages = apigwv2.get_stages(ApiId=api['ApiId'])

        for route in routes['Items']:
            apis.append({
                "type": "HTTP",
                "name": api['Name'],
                "route": route['RouteKey'],
                "api_id": api['ApiId'],
                "protocol": api['ProtocolType'],
            })

    print(f"\nAWS API 清单（{len(apis)} 个端点）：")
    for api in apis:
        print(f"  [{api['type']}] {api.get('name')} - {api.get('method', '')} {api.get('path', api.get('route', ''))}")

    return apis
```

### 步骤 5：API 版本与影子 API 检测

```python
def detect_shadow_and_zombie_apis(discovered_endpoints, documented_endpoints):
    """将发现的 API 与已记录的清单进行比对。"""

    # 规范化端点以便比对
    def normalize(ep):
        ep = re.sub(r'/v\d+/', '/vX/', ep)
        ep = re.sub(r'/\d+', '/{id}', ep)
        return ep.lower().rstrip('/')

    documented_normalized = {normalize(ep) for ep in documented_endpoints}

    shadow_apis = []  # 已发现但未记录
    zombie_apis = []  # 旧版本仍可访问

    for ep in discovered_endpoints:
        normalized = normalize(ep["url"])

        if normalized not in documented_normalized:
            # 检查是否为已记录 API 的旧版本
            if re.search(r'/v[0-9]+/', ep["url"]):
                zombie_apis.append(ep)
            else:
                shadow_apis.append(ep)

    print(f"\n影子 API（未记录）：{len(shadow_apis)} 个")
    for api in shadow_apis:
        print(f"  [影子] {api['url']} -> {api['status']}")

    print(f"\n僵尸 API（已废弃版本）：{len(zombie_apis)} 个")
    for api in zombie_apis:
        print(f"  [僵尸] {api['url']} -> {api['status']}")

    # 检查僵尸 API 是否缺少安全控制
    for api in zombie_apis:
        resp = requests.get(api["url"], timeout=5)
        if resp.status_code not in (401, 403):
            print(f"  [严重] 僵尸 API 无需认证即可访问：{api['url']}")

    return shadow_apis, zombie_apis
```

## 核心概念

| 术语 | 定义 |
|------|------|
| **影子 API（Shadow API）** | 由开发团队部署，未经过正式 API 管理流程或安全审查的 API |
| **僵尸 API（Zombie API）** | 已废弃或旧版本的 API，仍处于运行状态但不再维护或监控 |
| **API 清单（API Inventory）** | 组织中所有 API 的全面目录，包括端点 URL、所有者、版本、认证方式和数据分类 |
| **资产管理不当（Improper Inventory Management）** | OWASP API9:2023 - 未能维护准确的 API 清单，导致 API 端点未受监控和保护 |
| **攻击面（Attack Surface）** | 攻击者可能与之交互的所有 API 端点、方法和参数的总集合 |
| **API 蔓延（API Sprawl）** | 组织内 API 的无节制扩散，通常由微服务采用而缺乏集中治理所导致 |

## 工具与系统

- **Amass**：通过 DNS 枚举、网页爬取和 API 发现进行攻击面测绘的 OWASP 工具
- **httpx**：用于验证已发现域名并识别活跃 API 端点的快速 HTTP 探测工具
- **nuclei**：基于模板的扫描器，用于检测暴露的 API 文档、调试端点和配置错误的服务
- **Swagger UI Detector**：用于在组织内查找暴露的 Swagger/OpenAPI 文档端点的工具
- **Akto**：通过流量分析发现 API 并维护自动化清单的 API 安全平台

## 常见场景

### 场景：企业 API 攻击面评估

**场景背景**：一家大型企业有 200 多个开发团队使用微服务架构。安全团队怀疑有许多未记录的 API 暴露在互联网上，需要为安全审计进行全面的 API 清点。

**方法**：
1. DNS 枚举发现 340 个子域名，其中 45 个包含 API 相关关键词（api、rest、gateway、backend）
2. 使用 API 路径词典主动探测所有子域名，发现 127 个活跃 API 端点
3. 主 Web 应用的 JavaScript 分析揭示 34 个 API 端点，其中 8 个指向未记录的内部服务
4. AWS API Gateway 清点显示跨 12 个账户有 67 个 REST API 和 23 个 HTTP API
5. 与官方 API 目录交叉对比：31 个影子 API（未记录），14 个僵尸 API（已废弃版本）
6. 3 个僵尸 API 无认证，通过本应下线的端点暴露客户数据
7. 2 个影子 API 无需授权即可向互联网暴露内部管理功能

**常见陷阱**：
- 只检查已记录的 API 端点，遗漏在 API 网关之外部署的影子 API
- 未扫描前端应用程序硬编码 API 端点 URL 的 JavaScript 包
- 遗漏非标准端口或子路径后的 API
- 未检查多个 API 版本，其中旧版本可能缺乏安全控制
- 假设所有 API 都通过 API 网关，而实际上部分 API 可能直接暴露

## 输出格式

```
## API 清单与发现报告

**组织**：示例公司
**评估日期**：2024-12-15
**扫描域名**：340 个

### 摘要

| 类别 | 数量 |
|----------|-------|
| 发现的总 API 数 | 127 |
| 已记录的 API | 82 |
| 影子 API（未记录） | 31 |
| 僵尸 API（已废弃） | 14 |
| 无认证的 API | 8 |
| 暴露敏感数据的 API | 5 |

### 关键发现

1. **僵尸 API**：api-v1.example.com/api/v1/users - 2022 年已废弃，
   仍可访问，无需认证，返回完整用户数据
2. **影子 API**：internal-tools.example.com/api/admin - 管理功能
   无需授权暴露至互联网
3. **暴露的文档**：12 个 Swagger UI 实例可公开访问，
   泄露了完整的 API 模式和端点详情
```

