Skip to content

Commit 85c0dce

Browse files
committed
Add Feature-Gate Phase 1: weighted credit cost + 4 orchestration/bulk endpoints
Endpoints (25 -> 29 MCP tools): - GET /v1/audit/{domain} - full domain report + tech fingerprint + live headers - GET /v1/threat-report/{ip} - Shodan + AbuseIPDB + ASN + enrichment - POST /v1/cves/bulk - bulk CVE lookup (free 10, pro 50 per request) - POST /v1/iocs/bulk - bulk IOC enrichment (free 10, pro 50 per request) Rate limiting: - consume_credits(cost, max) - atomic N-credit consume via BEGIN IMMEDIATE - authenticate(request, endpoint, cost=1) - transparent per-endpoint cost - COST_AUDIT=4, COST_THREAT_REPORT=4 (reflects upstream call count) - X-RateLimit-Cost + X-RateLimit-Tier response headers Security hardening: - audit_domain: hard timeout guard (BULK_PER_DOMAIN_TIMEOUT) prevents worker hang - audit_domain: sensitive header filter (strips Set-Cookie, Authorization, X-API-Key etc from live_headers to prevent target-site secret leakage) - Rename TimeoutError -> FuturesTimeoutError alias for clarity Frontend: - Landing (EN + CN): 4 new endpoint cards + Credit Costs section + pricing update - Playground: audit + threat_report cards (bulk excluded, single-input precedent) Tests: 841 passed (+45 new - consume_credits atomicity, cost headers, exhaustion gating, bulk size limits, audit/threat-report handlers, 4 MCP tools)
1 parent 7b8b804 commit 85c0dce

24 files changed

+1712
-56
lines changed

CLAUDE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# CLAUDE.md — ContrastAPI
22

33
## Project
4-
Security intelligence API. 25 MCP tools, 35+ endpoints: CVE/EPSS/KEV, domain recon, IOC/threat intel, OSINT, code security.
4+
Security intelligence API. 29 MCP tools, 39+ endpoints: CVE/EPSS/KEV, domain recon, IOC/threat intel, OSINT, code security.
55
Live: api.contrastcyber.com | GitHub: UPinar/contrastapi
66

77
## Quick Reference

CONTRIBUTING.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Contributing to ContrastAPI
22

3-
Thanks for your interest in contributing! ContrastAPI is a security intelligence API with 25 MCP tools and 35+ endpoints.
3+
Thanks for your interest in contributing! ContrastAPI is a security intelligence API with 29 MCP tools and 39+ endpoints.
44

55
## Quick Start
66

@@ -32,7 +32,7 @@ app/
3232
├── codesec/ # Secret scanning, injection detection, headers
3333
│ ├── routes.py # Code security endpoints
3434
│ └── utils.py # Shared helpers (ReDoS-safe)
35-
├── mcp/ # MCP server (25 tools)
35+
├── mcp/ # MCP server (29 tools)
3636
│ └── server.py # Tool definitions + handlers
3737
├── templates/ # HTML templates (landing, docs, quickstart)
3838
├── static/ # CSS, JS, images

README.md

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,20 +7,20 @@
77
[![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](LICENSE)
88
[![Python 3.12](https://img.shields.io/badge/Python-3.12-blue.svg)](https://python.org)
99
[![Tests](https://img.shields.io/badge/Tests-782_passing-brightgreen.svg)](https://github.com/UPinar/contrastapi/actions)
10-
[![MCP](https://img.shields.io/badge/MCP-25_tools-purple.svg)](https://modelcontextprotocol.io)
10+
[![MCP](https://img.shields.io/badge/MCP-29_tools-purple.svg)](https://modelcontextprotocol.io)
1111
[![VS Code](https://img.shields.io/badge/VS_Code-Marketplace-007ACC.svg)](https://marketplace.visualstudio.com/items?itemName=ContrastAPI.contrastapi)
1212
[![RapidAPI](https://img.shields.io/badge/RapidAPI-Available-blue.svg)](https://rapidapi.com/UPinar/api/contrastapi)
1313
[![npm](https://img.shields.io/npm/v/contrastapi.svg)](https://www.npmjs.com/package/contrastapi)
1414

15-
**Security intelligence API and MCP server for AI agents.** 25 MCP tools / 35+ endpoints: CVE lookup with EPSS/KEV enrichment, domain reconnaissance, SSL analysis, IP reputation (AbuseIPDB, Shodan), IOC/malware lookup, exploit search, technology fingerprinting, email security, phone validation, and code security scanning. Free, no API key required.
15+
**Security intelligence API and MCP server for AI agents.** 29 MCP tools / 39+ endpoints: CVE lookup with EPSS/KEV enrichment, domain reconnaissance, SSL analysis, IP reputation (AbuseIPDB, Shodan), IOC/malware lookup, exploit search, technology fingerprinting, email security, phone validation, and code security scanning. Free, no API key required.
1616

1717
**English** | [中文](README_CN.md)
1818

1919
**Live:** [api.contrastcyber.com](https://api.contrastcyber.com) | **Quick Start:** [API](https://api.contrastcyber.com/quickstart) · [MCP](https://api.contrastcyber.com/mcp-setup) · [VS Code](https://marketplace.visualstudio.com/items?itemName=ContrastAPI.contrastapi) | **Docs:** [Endpoints](#endpoints) | **Scanner:** [contrastcyber.com](https://contrastcyber.com) | **Blog:** [I Built 25 Security Tools That AI Agents Can Use](https://dev.to/contrastcyber/i-built-23-security-tools-that-ai-agents-can-use-4he7)
2020

2121
## Use with AI Agents
2222

23-
**VS Code Extension:** Install [ContrastAPI](https://marketplace.visualstudio.com/items?itemName=ContrastAPI.contrastapi) from the Marketplace — 25 security tools in your editor, no API key required.
23+
**VS Code Extension:** Install [ContrastAPI](https://marketplace.visualstudio.com/items?itemName=ContrastAPI.contrastapi) from the Marketplace — 29 security tools in your editor, no API key required.
2424

2525
**MCP Setup** for Claude Desktop, Cursor, VS Code, Windsurf: **[MCP Setup Guide](https://api.contrastcyber.com/mcp-setup)**
2626

@@ -106,6 +106,8 @@ More examples: **[API Quick Start](https://api.contrastcyber.com/quickstart)** (
106106

107107
```
108108
GET /v1/domain/{domain} Full domain report (DNS + WHOIS + SSL + subs + WAF + reputation)
109+
GET /v1/audit/{domain} Comprehensive audit (full report + tech fingerprint + live headers)
110+
GET /v1/threat-report/{ip} Orchestrated IP threat report (Shodan + AbuseIPDB + ASN)
109111
GET /v1/dns/{domain} DNS records (A, AAAA, MX, NS, TXT, CNAME, SOA)
110112
GET /v1/whois/{domain} WHOIS registration data
111113
GET /v1/subdomains/{domain} Subdomain enumeration (DNS brute + CT logs)
@@ -133,6 +135,7 @@ GET /v1/cves/recent?hours=24 Latest CVEs
133135
GET /v1/cves/kev CISA exploited vulns
134136
GET /v1/epss/{cve_id} Exploit probability
135137
GET /v1/exploit/{cve_id} Public exploit search (GitHub Advisory + Shodan)
138+
POST /v1/cves/bulk Bulk CVE lookup (10 free, 50 pro)
136139
```
137140

138141
### Threat Intelligence
@@ -142,6 +145,7 @@ GET /v1/ioc/{indicator} Unified IOC enrichment (IP, domain, URL, hash)
142145
GET /v1/hash/{hash} Malware hash reputation (MalwareBazaar)
143146
GET /v1/password/{sha1} Password breach check (HIBP, k-anonymity)
144147
GET /v1/phishing/{url} Phishing/malware URL check (URLhaus)
148+
POST /v1/iocs/bulk Bulk IOC enrichment (10 free, 50 pro)
145149
GET /v1/phone/{number} Phone number OSINT (carrier, type, country)
146150
GET /v1/username/{username} Username OSINT (16 platforms, account discovery)
147151
```
@@ -162,6 +166,19 @@ POST /v1/check/dependencies Check packages for known CVEs
162166
| Free | 100 req/hr | Not required |
163167
| Pro | 1,000 req/hr | [Get API Key](https://contrastcyber.com/pricing) |
164168

169+
### Credit Costs
170+
171+
Most endpoints consume **1 credit** per call. Aggregating endpoints that fan out to multiple upstream sources cost more:
172+
173+
| Endpoint | Cost |
174+
|----------|------|
175+
| Most endpoints | 1 |
176+
| `GET /v1/audit/{domain}` | 4 |
177+
| `GET /v1/threat-report/{ip}` | 4 |
178+
| Bulk endpoints (`/v1/cves/bulk`, `/v1/iocs/bulk`) | N (one per item) |
179+
180+
Every authenticated response includes an `X-RateLimit-Cost` header so you can track usage transparently alongside `X-RateLimit-Remaining`.
181+
165182
## Data Sources
166183

167184
| Source | Records | Update |

README_CN.md

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,12 @@
77
[![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](LICENSE)
88
[![Python 3.12](https://img.shields.io/badge/Python-3.12-blue.svg)](https://python.org)
99
[![Tests](https://img.shields.io/badge/Tests-782_passing-brightgreen.svg)](https://github.com/UPinar/contrastapi/actions)
10-
[![MCP](https://img.shields.io/badge/MCP-25_tools-purple.svg)](https://modelcontextprotocol.io)
10+
[![MCP](https://img.shields.io/badge/MCP-29_tools-purple.svg)](https://modelcontextprotocol.io)
1111
[![VS Code](https://img.shields.io/badge/VS_Code-Marketplace-007ACC.svg)](https://marketplace.visualstudio.com/items?itemName=ContrastAPI.contrastapi)
1212
[![RapidAPI](https://img.shields.io/badge/RapidAPI-Available-blue.svg)](https://rapidapi.com/UPinar/api/contrastapi)
1313
[![npm](https://img.shields.io/npm/v/contrastapi.svg)](https://www.npmjs.com/package/contrastapi)
1414

15-
**安全情报 API 和 AI 智能体 MCP 服务器。** 25 个 MCP 工具 / 35+ 个端点:CVE 查询(含 EPSS/KEV 增强)、域名侦察、SSL 分析、IP 信誉(AbuseIPDB、Shodan)、IOC/恶意软件查询、漏洞利用搜索、技术指纹识别、电子邮件安全、电话号码验证和代码安全扫描。免费使用,无需 API 密钥。
15+
**安全情报 API 和 AI 智能体 MCP 服务器。** 29 个 MCP 工具 / 39+ 个端点:CVE 查询(含 EPSS/KEV 增强)、域名侦察、SSL 分析、IP 信誉(AbuseIPDB、Shodan)、IOC/恶意软件查询、漏洞利用搜索、技术指纹识别、电子邮件安全、电话号码验证和代码安全扫描。免费使用,无需 API 密钥。
1616

1717
**在线服务:** [api.contrastcyber.com](https://api.contrastcyber.com) | **快速入门:** [API](https://api.contrastcyber.com/quickstart) · [MCP](https://api.contrastcyber.com/mcp-setup) · [VS Code](https://marketplace.visualstudio.com/items?itemName=ContrastAPI.contrastapi) | **文档:** [接口列表](#接口列表) | **扫描器:** [contrastcyber.com](https://contrastcyber.com)
1818

@@ -24,7 +24,7 @@
2424

2525
## 与 AI 智能体配合使用
2626

27-
**VS Code 扩展:**[Marketplace](https://marketplace.visualstudio.com/items?itemName=ContrastAPI.contrastapi) 安装 ContrastAPI — 编辑器内 25 个安全工具,无需 API 密钥。
27+
**VS Code 扩展:**[Marketplace](https://marketplace.visualstudio.com/items?itemName=ContrastAPI.contrastapi) 安装 ContrastAPI — 编辑器内 29 个安全工具,无需 API 密钥。
2828

2929
**MCP 配置** 支持 Claude Desktop、Cursor、VS Code、Windsurf 等工具:**[MCP 配置指南](https://api.contrastcyber.com/mcp-setup)**
3030

@@ -120,6 +120,8 @@ curl https://api.contrastcyber.com/v1/domain/example.com
120120

121121
```
122122
GET /v1/domain/{domain} 完整域名报告(DNS + WHOIS + SSL + 子域名 + WAF + 信誉)
123+
GET /v1/audit/{domain} 综合审计(完整报告 + 技术指纹 + 实时响应头)
124+
GET /v1/threat-report/{ip} 编排式 IP 威胁报告(Shodan + AbuseIPDB + ASN)
123125
GET /v1/dns/{domain} DNS 记录(A、AAAA、MX、NS、TXT、CNAME、SOA)
124126
GET /v1/whois/{domain} WHOIS 注册信息
125127
GET /v1/subdomains/{domain} 子域名枚举(DNS 爆破 + CT 日志)
@@ -147,6 +149,7 @@ GET /v1/cves/recent?hours=24 最新 CVE
147149
GET /v1/cves/kev CISA 已利用漏洞
148150
GET /v1/epss/{cve_id} 漏洞利用概率
149151
GET /v1/exploit/{cve_id} 公开漏洞利用搜索(GitHub Advisory + Shodan)
152+
POST /v1/cves/bulk 批量 CVE 查询(免费 10 个,Pro 50 个)
150153
```
151154

152155
### 威胁情报
@@ -156,6 +159,7 @@ GET /v1/ioc/{indicator} 统一 IOC 查询(IP、域名、URL、哈希)
156159
GET /v1/hash/{hash} 恶意软件哈希信誉(MalwareBazaar)
157160
GET /v1/password/{sha1} 密码泄露检查(HIBP,k-匿名)
158161
GET /v1/phishing/{url} 钓鱼/恶意 URL 检查(URLhaus)
162+
POST /v1/iocs/bulk 批量 IOC 富化(免费 10 个,Pro 50 个)
159163
GET /v1/phone/{number} 电话号码 OSINT(运营商、类型、国家)
160164
```
161165

app/auth.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
from config import FREE_HOURLY_LIMIT, KEY_LENGTH, KEY_PREFIX, PRO_HOURLY_LIMIT
1212
from db import get_api_key, log_usage, touch_api_key
1313
from fastapi import HTTPException, Request
14-
from ratelimit import check_limit_with_count, get_reset_time
14+
from ratelimit import consume_credits, get_reset_time
1515

1616

1717
def generate_key() -> str:
@@ -41,7 +41,7 @@ def extract_key(request: Request) -> str | None:
4141
return None
4242

4343

44-
def authenticate(request: Request, endpoint: str) -> dict:
44+
def authenticate(request: Request, endpoint: str, cost: int = 1) -> dict:
4545
"""Authenticate request. Returns auth context dict.
4646
4747
For Pro keys: verifies key, checks hourly limit (1000/hr).
@@ -60,6 +60,7 @@ def authenticate(request: Request, endpoint: str) -> dict:
6060
raw_key = extract_key(request)
6161

6262
localhost = client_ip in ("127.0.0.1", "::1")
63+
request.state.ratelimit_cost = cost
6364

6465
if raw_key:
6566
# Pro key authentication
@@ -74,7 +75,7 @@ def authenticate(request: Request, endpoint: str) -> dict:
7475

7576
if not localhost:
7677
# Check Pro rate limit (sliding window)
77-
allowed, remaining = check_limit_with_count("api", store_key, limit)
78+
allowed, remaining = consume_credits("api", store_key, cost, limit)
7879
if not allowed:
7980
_set_ratelimit_state(request, advertised_limit, 0, get_reset_time("api", store_key))
8081
raise HTTPException(
@@ -88,6 +89,7 @@ def authenticate(request: Request, endpoint: str) -> dict:
8889
touch_api_key(kh)
8990
if not localhost:
9091
log_usage(client_ip, endpoint, key_hash=kh)
92+
request.state.ratelimit_tier = "pro"
9193
return {"tier": "pro", "key_hash": kh, "client_ip": client_ip}
9294

9395
# Keyless — IP rate limit (sliding window)
@@ -96,7 +98,7 @@ def authenticate(request: Request, endpoint: str) -> dict:
9698
store_key = f"free:{client_ip}"
9799

98100
if not localhost:
99-
allowed, remaining = check_limit_with_count("api", store_key, limit)
101+
allowed, remaining = consume_credits("api", store_key, cost, limit)
100102
if not allowed:
101103
_set_ratelimit_state(request, advertised_limit, 0, get_reset_time("api", store_key))
102104
raise HTTPException(
@@ -110,4 +112,5 @@ def authenticate(request: Request, endpoint: str) -> dict:
110112

111113
if not localhost:
112114
log_usage(client_ip, endpoint)
115+
request.state.ratelimit_tier = "free"
113116
return {"tier": "free", "key_hash": None, "client_ip": client_ip}

app/config.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,13 @@
3636
PRO_BULK_LIMIT = 50 # max domains per bulk request (pro)
3737
ENRICHMENT_DAILY_LIMIT = 10 # enriched scans per IP per day (protects external API quotas)
3838

39+
# Endpoint credit costs — based on upstream API calls per request.
40+
# Default is 1 (single upstream). Orchestration endpoints cost more because
41+
# they aggregate multiple sources. Transparent pricing, surfaced via X-RateLimit-Cost header.
42+
COST_DEFAULT = 1
43+
COST_AUDIT = 4 # domain_report + live_headers + tech_detect + cache layer
44+
COST_THREAT_REPORT = 4 # ip_enrichment + abuseipdb + shodan + asn
45+
3946
# API key
4047
KEY_PREFIX = "cc_"
4148
KEY_LENGTH = 48 # hex chars after prefix

app/cve/routes.py

Lines changed: 95 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,16 @@
66
from auth import authenticate
77
from db import get_cached_domain, get_cve, get_epss, get_kev_cves, get_recent_cves, save_cached_domain, search_cves
88
from fastapi import APIRouter, HTTPException, Query, Request
9-
from schemas import CveKevResponse, CveRecentResponse, CveResponse, CveSearchResponse, EpssResponse, ExploitResponse
9+
from pydantic import BaseModel, Field
10+
from schemas import (
11+
BulkCveResponse,
12+
CveKevResponse,
13+
CveRecentResponse,
14+
CveResponse,
15+
CveSearchResponse,
16+
EpssResponse,
17+
ExploitResponse,
18+
)
1019
from validation import is_valid_ip, validate_cve_id
1120

1221
logger = logging.getLogger("contrastapi")
@@ -347,3 +356,88 @@ def exploit_lookup(cve_id: str, request: Request):
347356

348357
save_cached_domain(cache_key, result)
349358
return {**result, "cached": False}
359+
360+
361+
# === Bulk CVE Lookup ===
362+
363+
364+
class _BulkCveRequest(BaseModel):
365+
cve_ids: list[str] = Field(..., min_length=1, max_length=50)
366+
367+
368+
@router.post(
369+
"/cves/bulk",
370+
operation_id="bulk_cve_lookup",
371+
response_model=BulkCveResponse,
372+
response_model_exclude_none=True,
373+
)
374+
def bulk_cve_lookup(body: _BulkCveRequest, request: Request):
375+
"""Bulk CVE lookup — up to 10 CVEs (free) or 50 (pro). Each CVE counts as 1 request toward rate limit."""
376+
import ratelimit
377+
from auth import extract_key, hash_key
378+
from config import FREE_BULK_LIMIT, FREE_HOURLY_LIMIT, PRO_BULK_LIMIT, PRO_HOURLY_LIMIT
379+
from validation import get_client_ip
380+
381+
auth_ctx = authenticate(request, "/v1/cves/bulk")
382+
client_ip = get_client_ip(request)
383+
384+
bulk_limit = PRO_BULK_LIMIT if auth_ctx["tier"] == "pro" else FREE_BULK_LIMIT
385+
386+
cve_ids = list(dict.fromkeys(c.strip().upper() for c in body.cve_ids if c.strip()))
387+
count = len(cve_ids)
388+
389+
if count == 0:
390+
raise HTTPException(status_code=400, detail="cve_ids must contain at least one valid CVE ID")
391+
if count > bulk_limit:
392+
raise HTTPException(
393+
status_code=422,
394+
detail=f"Too many CVE IDs. Limit: {bulk_limit} (your tier: {auth_ctx['tier']})",
395+
)
396+
397+
for cid in cve_ids:
398+
if not validate_cve_id(cid):
399+
raise HTTPException(status_code=400, detail=f"Invalid CVE ID format: {cid}")
400+
401+
raw_key = extract_key(request)
402+
if raw_key:
403+
store_key = f"pro:{hash_key(raw_key)}"
404+
limit = PRO_HOURLY_LIMIT
405+
else:
406+
store_key = f"free:{client_ip}"
407+
limit = FREE_HOURLY_LIMIT
408+
409+
if count > 1 and not ratelimit.consume_bulk("api", store_key, count - 1, limit):
410+
raise HTTPException(
411+
status_code=429,
412+
detail=f"Insufficient rate limit quota for {count} CVE IDs.",
413+
)
414+
415+
results = []
416+
successful = 0
417+
for cid in cve_ids:
418+
try:
419+
row = get_cve(cid)
420+
if row is None:
421+
results.append({"cve_id": cid, "status": "not_found", "cve": None, "error": f"CVE {cid} not found"})
422+
else:
423+
results.append({"cve_id": cid, "status": "ok", "cve": _format_cve(row), "error": None})
424+
successful += 1
425+
except Exception as e:
426+
logger.warning("Bulk CVE lookup failed for %s: %s", cid, type(e).__name__)
427+
results.append({"cve_id": cid, "status": "error", "cve": None, "error": "Lookup failed"})
428+
429+
failed = count - successful
430+
if failed == 0:
431+
summary = f"All {count} CVEs found"
432+
elif successful == 0:
433+
summary = f"No CVEs found in {count} lookups"
434+
else:
435+
summary = f"{successful}/{count} CVEs found, {failed} not found or failed"
436+
437+
return {
438+
"results": results,
439+
"total": count,
440+
"successful": successful,
441+
"failed": failed,
442+
"summary": summary,
443+
}

0 commit comments

Comments
 (0)