contrastapi 1.22.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- contrastapi/__init__.py +39 -0
- contrastapi/_transport.py +170 -0
- contrastapi/_version.py +1 -0
- contrastapi/client.py +171 -0
- contrastapi/exceptions.py +176 -0
- contrastapi/models.py +661 -0
- contrastapi/namespaces/__init__.py +0 -0
- contrastapi/namespaces/atlas.py +134 -0
- contrastapi/namespaces/cve.py +125 -0
- contrastapi/namespaces/d3fend.py +124 -0
- contrastapi/namespaces/domain.py +129 -0
- contrastapi/namespaces/intel.py +151 -0
- contrastapi/namespaces/ioc.py +61 -0
- contrastapi/namespaces/security.py +94 -0
- contrastapi/shortcuts.py +182 -0
- contrastapi-1.22.3.dist-info/METADATA +194 -0
- contrastapi-1.22.3.dist-info/RECORD +18 -0
- contrastapi-1.22.3.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"""IOC namespace — indicator lookup, hash, phishing, bulk."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from urllib.parse import quote
|
|
6
|
+
|
|
7
|
+
from .._transport import _AsyncTransport, _SyncTransport
|
|
8
|
+
from ..models import BulkIocResponse, HashResponse, IocResponse, PhishingResponse
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _enc(value: str) -> str:
|
|
12
|
+
if value is None or value == "":
|
|
13
|
+
raise ValueError("Missing required parameter")
|
|
14
|
+
return quote(str(value), safe="")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _enc_path(value: str) -> str:
|
|
18
|
+
"""For URL-typed indicators (paths) — preserve `/` as a separator."""
|
|
19
|
+
if value is None or value == "":
|
|
20
|
+
raise ValueError("Missing required parameter")
|
|
21
|
+
return "/".join(quote(seg, safe="") for seg in str(value).split("/"))
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _validate_bulk(items: list[str]) -> list[str]:
|
|
25
|
+
if not isinstance(items, list) or not all(isinstance(i, str) for i in items):
|
|
26
|
+
raise ValueError("indicators must be a list of strings")
|
|
27
|
+
return items
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class Ioc:
|
|
31
|
+
def __init__(self, transport: _SyncTransport) -> None:
|
|
32
|
+
self._t = transport
|
|
33
|
+
|
|
34
|
+
def lookup(self, indicator: str) -> IocResponse:
|
|
35
|
+
return self._t.get(f"/v1/ioc/{_enc_path(indicator)}")
|
|
36
|
+
|
|
37
|
+
def hash(self, file_hash: str) -> HashResponse:
|
|
38
|
+
return self._t.get(f"/v1/hash/{_enc(file_hash)}")
|
|
39
|
+
|
|
40
|
+
def phishing(self, url: str) -> PhishingResponse:
|
|
41
|
+
return self._t.get(f"/v1/phishing/{_enc_path(url)}")
|
|
42
|
+
|
|
43
|
+
def bulk(self, indicators: list[str]) -> BulkIocResponse:
|
|
44
|
+
return self._t.post("/v1/iocs/bulk", json_body={"indicators": _validate_bulk(indicators)})
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class AsyncIoc:
|
|
48
|
+
def __init__(self, transport: _AsyncTransport) -> None:
|
|
49
|
+
self._t = transport
|
|
50
|
+
|
|
51
|
+
async def lookup(self, indicator: str) -> IocResponse:
|
|
52
|
+
return await self._t.aget(f"/v1/ioc/{_enc_path(indicator)}")
|
|
53
|
+
|
|
54
|
+
async def hash(self, file_hash: str) -> HashResponse:
|
|
55
|
+
return await self._t.aget(f"/v1/hash/{_enc(file_hash)}")
|
|
56
|
+
|
|
57
|
+
async def phishing(self, url: str) -> PhishingResponse:
|
|
58
|
+
return await self._t.aget(f"/v1/phishing/{_enc_path(url)}")
|
|
59
|
+
|
|
60
|
+
async def bulk(self, indicators: list[str]) -> BulkIocResponse:
|
|
61
|
+
return await self._t.apost("/v1/iocs/bulk", json_body={"indicators": _validate_bulk(indicators)})
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"""Code-security + live-scan namespaces."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
from urllib.parse import quote
|
|
7
|
+
|
|
8
|
+
from .._transport import _AsyncTransport, _SyncTransport
|
|
9
|
+
from ..models import (
|
|
10
|
+
CheckHeadersResponse,
|
|
11
|
+
CodeCheckResponse,
|
|
12
|
+
DependenciesResponse,
|
|
13
|
+
ScanHeadersResponse,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _enc(value: str) -> str:
|
|
18
|
+
if value is None or value == "":
|
|
19
|
+
raise ValueError("Missing required parameter")
|
|
20
|
+
return quote(str(value), safe="")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _validate_packages(packages: list[str]) -> list[str]:
|
|
24
|
+
if not isinstance(packages, list) or not all(isinstance(p, str) for p in packages):
|
|
25
|
+
raise ValueError("packages must be a list of strings")
|
|
26
|
+
return packages
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _validate_headers(headers: dict[str, str]) -> dict[str, str]:
|
|
30
|
+
if not isinstance(headers, dict):
|
|
31
|
+
raise ValueError("headers must be a dict[str, str]")
|
|
32
|
+
return headers
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class Check:
|
|
36
|
+
def __init__(self, transport: _SyncTransport) -> None:
|
|
37
|
+
self._t = transport
|
|
38
|
+
|
|
39
|
+
def secrets(self, code: str, language: str | None = None) -> CodeCheckResponse:
|
|
40
|
+
body: dict[str, Any] = {"code": code}
|
|
41
|
+
if language is not None:
|
|
42
|
+
body["language"] = language
|
|
43
|
+
return self._t.post("/v1/check/secrets", json_body=body)
|
|
44
|
+
|
|
45
|
+
def injection(self, code: str, language: str | None = None) -> CodeCheckResponse:
|
|
46
|
+
body: dict[str, Any] = {"code": code}
|
|
47
|
+
if language is not None:
|
|
48
|
+
body["language"] = language
|
|
49
|
+
return self._t.post("/v1/check/injection", json_body=body)
|
|
50
|
+
|
|
51
|
+
def headers(self, headers: dict[str, str]) -> CheckHeadersResponse:
|
|
52
|
+
return self._t.post("/v1/check/headers", json_body={"headers": _validate_headers(headers)})
|
|
53
|
+
|
|
54
|
+
def dependencies(self, packages: list[str]) -> DependenciesResponse:
|
|
55
|
+
return self._t.post("/v1/check/dependencies", json_body={"packages": _validate_packages(packages)})
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class Scan:
|
|
59
|
+
def __init__(self, transport: _SyncTransport) -> None:
|
|
60
|
+
self._t = transport
|
|
61
|
+
|
|
62
|
+
def headers(self, domain: str) -> ScanHeadersResponse:
|
|
63
|
+
return self._t.get(f"/v1/scan/headers/{_enc(domain)}")
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class AsyncCheck:
|
|
67
|
+
def __init__(self, transport: _AsyncTransport) -> None:
|
|
68
|
+
self._t = transport
|
|
69
|
+
|
|
70
|
+
async def secrets(self, code: str, language: str | None = None) -> CodeCheckResponse:
|
|
71
|
+
body: dict[str, Any] = {"code": code}
|
|
72
|
+
if language is not None:
|
|
73
|
+
body["language"] = language
|
|
74
|
+
return await self._t.apost("/v1/check/secrets", json_body=body)
|
|
75
|
+
|
|
76
|
+
async def injection(self, code: str, language: str | None = None) -> CodeCheckResponse:
|
|
77
|
+
body: dict[str, Any] = {"code": code}
|
|
78
|
+
if language is not None:
|
|
79
|
+
body["language"] = language
|
|
80
|
+
return await self._t.apost("/v1/check/injection", json_body=body)
|
|
81
|
+
|
|
82
|
+
async def headers(self, headers: dict[str, str]) -> CheckHeadersResponse:
|
|
83
|
+
return await self._t.apost("/v1/check/headers", json_body={"headers": _validate_headers(headers)})
|
|
84
|
+
|
|
85
|
+
async def dependencies(self, packages: list[str]) -> DependenciesResponse:
|
|
86
|
+
return await self._t.apost("/v1/check/dependencies", json_body={"packages": _validate_packages(packages)})
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class AsyncScan:
|
|
90
|
+
def __init__(self, transport: _AsyncTransport) -> None:
|
|
91
|
+
self._t = transport
|
|
92
|
+
|
|
93
|
+
async def headers(self, domain: str) -> ScanHeadersResponse:
|
|
94
|
+
return await self._t.aget(f"/v1/scan/headers/{_enc(domain)}")
|
contrastapi/shortcuts.py
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
"""High-level convenience helpers that compose multiple namespace calls.
|
|
2
|
+
|
|
3
|
+
These wrap common multi-call patterns so callers don't have to glue endpoints
|
|
4
|
+
together by hand. Each helper accepts a `ContrastAPI` (sync) instance; async
|
|
5
|
+
variants are intentionally omitted in this batch — call the async client's
|
|
6
|
+
namespace methods directly with `asyncio.gather` if you need parallelism.
|
|
7
|
+
|
|
8
|
+
Convention: shortcuts swallow per-leg ContrastAPIError instances and report
|
|
9
|
+
them in the returned dict under `errors[<key>]` so a partial failure doesn't
|
|
10
|
+
nuke the whole report. The caller still gets every leg that succeeded.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import ipaddress
|
|
16
|
+
import re
|
|
17
|
+
from typing import Any
|
|
18
|
+
|
|
19
|
+
from .client import ContrastAPI
|
|
20
|
+
from .exceptions import ContrastAPIError, TransportError
|
|
21
|
+
|
|
22
|
+
_HEX64 = re.compile(r"^[0-9a-fA-F]{32,128}$")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _record_or_reraise(out: dict[str, Any], key: str, exc: ContrastAPIError) -> None:
|
|
26
|
+
"""Record a per-leg failure into the helper's error dict — but propagate
|
|
27
|
+
`TransportError` (network/MITM/DNS-poison) so security incidents aren't
|
|
28
|
+
masked as opaque error strings. Application-layer errors (4xx/5xx) keep
|
|
29
|
+
the swallow-and-report behaviour callers depend on for partial-success
|
|
30
|
+
reports.
|
|
31
|
+
"""
|
|
32
|
+
if isinstance(exc, TransportError):
|
|
33
|
+
raise exc
|
|
34
|
+
out["errors"][key] = exc.message
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _classify_ioc(value: str) -> str:
|
|
38
|
+
"""Best-effort IOC type detection: 'ip' | 'hash' | 'domain' | 'unknown'.
|
|
39
|
+
|
|
40
|
+
Mirrors the server's input router so callers see the same routing the API
|
|
41
|
+
would apply. Domain is the broad fallback (anything with a dot or letters).
|
|
42
|
+
"""
|
|
43
|
+
value = value.strip()
|
|
44
|
+
try:
|
|
45
|
+
ipaddress.ip_address(value)
|
|
46
|
+
return "ip"
|
|
47
|
+
except ValueError:
|
|
48
|
+
pass
|
|
49
|
+
if _HEX64.match(value):
|
|
50
|
+
return "hash"
|
|
51
|
+
if "." in value or value.replace("-", "").isalnum():
|
|
52
|
+
return "domain"
|
|
53
|
+
return "unknown"
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def triage_ioc(client: ContrastAPI, indicator: str) -> dict[str, Any]:
|
|
57
|
+
"""Run the right enrichment for a single IOC.
|
|
58
|
+
|
|
59
|
+
For an IP: ioc_lookup + threat_report.
|
|
60
|
+
For a domain: ioc_lookup + domain_report.
|
|
61
|
+
For a hash: ioc_lookup + hash_lookup.
|
|
62
|
+
Any unrecognised input still gets ioc_lookup (the server is the final word).
|
|
63
|
+
|
|
64
|
+
Returns:
|
|
65
|
+
{"indicator": ..., "kind": ..., "ioc": {...}, "threat_report"|"domain_report"|"hash": {...},
|
|
66
|
+
"errors": {<leg>: <error message>}}
|
|
67
|
+
"""
|
|
68
|
+
kind = _classify_ioc(indicator)
|
|
69
|
+
out: dict[str, Any] = {"indicator": indicator, "kind": kind, "errors": {}}
|
|
70
|
+
|
|
71
|
+
try:
|
|
72
|
+
out["ioc"] = client.ioc.lookup(indicator)
|
|
73
|
+
except ContrastAPIError as exc:
|
|
74
|
+
_record_or_reraise(out, "ioc", exc)
|
|
75
|
+
|
|
76
|
+
if kind == "ip":
|
|
77
|
+
try:
|
|
78
|
+
out["threat_report"] = client.ip.threat_report(indicator)
|
|
79
|
+
except ContrastAPIError as exc:
|
|
80
|
+
_record_or_reraise(out, "threat_report", exc)
|
|
81
|
+
elif kind == "domain":
|
|
82
|
+
try:
|
|
83
|
+
out["domain_report"] = client.domain.report(indicator)
|
|
84
|
+
except ContrastAPIError as exc:
|
|
85
|
+
_record_or_reraise(out, "domain_report", exc)
|
|
86
|
+
elif kind == "hash":
|
|
87
|
+
try:
|
|
88
|
+
out["hash"] = client.ioc.hash(indicator)
|
|
89
|
+
except ContrastAPIError as exc:
|
|
90
|
+
_record_or_reraise(out, "hash", exc)
|
|
91
|
+
|
|
92
|
+
return out
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def audit_full(
|
|
96
|
+
client: ContrastAPI,
|
|
97
|
+
domain: str,
|
|
98
|
+
*,
|
|
99
|
+
ssl_subdomains: int = 5,
|
|
100
|
+
) -> dict[str, Any]:
|
|
101
|
+
"""Run a deep audit on a domain.
|
|
102
|
+
|
|
103
|
+
Composes `audit_domain` + `subdomain_enum` (top-N) + `tech_fingerprint` +
|
|
104
|
+
per-subdomain `ssl_check` (capped at `ssl_subdomains` to keep credit cost
|
|
105
|
+
bounded — default 5).
|
|
106
|
+
|
|
107
|
+
Returns a dict with `audit`, `subdomains`, `tech`, `ssl` (per-subdomain
|
|
108
|
+
map), and `errors` (per-leg failure messages).
|
|
109
|
+
"""
|
|
110
|
+
if ssl_subdomains < 0:
|
|
111
|
+
raise ValueError("ssl_subdomains must be >= 0")
|
|
112
|
+
|
|
113
|
+
out: dict[str, Any] = {"domain": domain, "errors": {}, "ssl": {}}
|
|
114
|
+
|
|
115
|
+
try:
|
|
116
|
+
out["audit"] = client.domain.audit(domain)
|
|
117
|
+
except ContrastAPIError as exc:
|
|
118
|
+
_record_or_reraise(out, "audit", exc)
|
|
119
|
+
|
|
120
|
+
subdomains: list[str] = []
|
|
121
|
+
try:
|
|
122
|
+
sub_response = client.domain.subdomains(domain)
|
|
123
|
+
out["subdomains"] = sub_response
|
|
124
|
+
# Server response shape: {"subdomains": [...], "count": N, ...}
|
|
125
|
+
candidates = sub_response.get("subdomains") or []
|
|
126
|
+
subdomains = [s for s in candidates if isinstance(s, str)][:ssl_subdomains]
|
|
127
|
+
except ContrastAPIError as exc:
|
|
128
|
+
_record_or_reraise(out, "subdomains", exc)
|
|
129
|
+
|
|
130
|
+
try:
|
|
131
|
+
out["tech"] = client.domain.tech(domain)
|
|
132
|
+
except ContrastAPIError as exc:
|
|
133
|
+
_record_or_reraise(out, "tech", exc)
|
|
134
|
+
|
|
135
|
+
for sub in subdomains:
|
|
136
|
+
try:
|
|
137
|
+
out["ssl"][sub] = client.domain.ssl(sub)
|
|
138
|
+
except ContrastAPIError as exc:
|
|
139
|
+
_record_or_reraise(out, f"ssl:{sub}", exc)
|
|
140
|
+
|
|
141
|
+
return out
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def enrich_batch(client: ContrastAPI, items: list[str]) -> dict[str, Any]:
|
|
145
|
+
"""Auto-detect IOC vs CVE for each item and route to the right bulk endpoint.
|
|
146
|
+
|
|
147
|
+
Items matching the `CVE-YYYY-NNNN+` pattern go to `cve.bulk`; everything
|
|
148
|
+
else goes to `ioc.bulk`. If both buckets are non-empty the helper makes two
|
|
149
|
+
calls; if only one bucket has items it makes a single call. Returns:
|
|
150
|
+
|
|
151
|
+
{"cve": <bulk_cve_response or None>, "ioc": <bulk_ioc_response or None>,
|
|
152
|
+
"routed": {"cve": [...], "ioc": [...]}, "errors": {...}}
|
|
153
|
+
"""
|
|
154
|
+
if not isinstance(items, list) or not all(isinstance(i, str) for i in items):
|
|
155
|
+
raise ValueError("items must be a list of strings")
|
|
156
|
+
|
|
157
|
+
# Match server-side validator: uppercase canonical, NVD spec allows 3+
|
|
158
|
+
# digit suffix (e.g. CVE-2024-123 is valid). Lowercase routes to IOC bucket.
|
|
159
|
+
cve_pattern = re.compile(r"^CVE-\d{4}-\d{3,}$")
|
|
160
|
+
cves = [i for i in items if cve_pattern.match(i)]
|
|
161
|
+
iocs = [i for i in items if not cve_pattern.match(i)]
|
|
162
|
+
|
|
163
|
+
out: dict[str, Any] = {
|
|
164
|
+
"cve": None,
|
|
165
|
+
"ioc": None,
|
|
166
|
+
"routed": {"cve": cves, "ioc": iocs},
|
|
167
|
+
"errors": {},
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if cves:
|
|
171
|
+
try:
|
|
172
|
+
out["cve"] = client.cve.bulk(cves)
|
|
173
|
+
except ContrastAPIError as exc:
|
|
174
|
+
_record_or_reraise(out, "cve", exc)
|
|
175
|
+
|
|
176
|
+
if iocs:
|
|
177
|
+
try:
|
|
178
|
+
out["ioc"] = client.ioc.bulk(iocs)
|
|
179
|
+
except ContrastAPIError as exc:
|
|
180
|
+
_record_or_reraise(out, "ioc", exc)
|
|
181
|
+
|
|
182
|
+
return out
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: contrastapi
|
|
3
|
+
Version: 1.22.3
|
|
4
|
+
Summary: Official Python SDK for ContrastAPI — security intelligence for developers and AI agents
|
|
5
|
+
Project-URL: Homepage, https://contrastcyber.com
|
|
6
|
+
Project-URL: Documentation, https://api.contrastcyber.com/docs
|
|
7
|
+
Project-URL: Repository, https://github.com/UPinar/contrastapi
|
|
8
|
+
Project-URL: Issues, https://github.com/UPinar/contrastapi/issues
|
|
9
|
+
Author-email: ContrastCyber <contact@contrastcyber.com>
|
|
10
|
+
License-Expression: MIT
|
|
11
|
+
Keywords: ai-security,api,atlas,attack,cve,cwe,d3fend,dns,domain,kev,mcp,mitre,osint,security,ssl,threat-intelligence,vulnerability,whois
|
|
12
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Topic :: Security
|
|
22
|
+
Classifier: Typing :: Typed
|
|
23
|
+
Requires-Python: >=3.10
|
|
24
|
+
Requires-Dist: httpx<1.0,>=0.25
|
|
25
|
+
Provides-Extra: test
|
|
26
|
+
Requires-Dist: pytest-asyncio>=0.21; extra == 'test'
|
|
27
|
+
Requires-Dist: pytest>=7; extra == 'test'
|
|
28
|
+
Requires-Dist: respx>=0.20; extra == 'test'
|
|
29
|
+
Description-Content-Type: text/markdown
|
|
30
|
+
|
|
31
|
+
# ContrastAPI Python SDK
|
|
32
|
+
|
|
33
|
+
Official Python client for [ContrastAPI](https://api.contrastcyber.com) — security intelligence for developers and AI agents.
|
|
34
|
+
|
|
35
|
+
42 MCP tools / 50+ HTTP endpoints: CVE / EPSS / KEV / CWE, MITRE ATLAS (AI/ML attacks) + bulk drill, MITRE D3FEND defenses, domain audit, IOC + threat intel, OSINT, code-security checks, and more. No API key required for the free tier (100 req/hr).
|
|
36
|
+
|
|
37
|
+
## Install
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
pip install contrastapi
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Requires Python 3.10+. Depends only on `httpx>=0.25`.
|
|
44
|
+
|
|
45
|
+
## Quickstart
|
|
46
|
+
|
|
47
|
+
### Sync
|
|
48
|
+
|
|
49
|
+
```python
|
|
50
|
+
from contrastapi import ContrastAPI
|
|
51
|
+
from contrastapi.models import CveResponse, AuditResponse
|
|
52
|
+
|
|
53
|
+
with ContrastAPI() as client: # keyless, free tier
|
|
54
|
+
cve: CveResponse = client.cve.lookup("CVE-2021-44228")
|
|
55
|
+
print(cve["kev"]["in_kev"]) # True (IDE autocompletes "kev", "epss"...)
|
|
56
|
+
|
|
57
|
+
techs = client.atlas.bulk_technique_lookup(["AML.T0051", "AML.T0043"])
|
|
58
|
+
print(techs["successful"]) # 2
|
|
59
|
+
|
|
60
|
+
audit: AuditResponse = client.domain.audit("example.com")
|
|
61
|
+
print(audit["score"])
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Every response method has a typed return — `client.cve.lookup(...)` returns `CveResponse`, `client.atlas.technique(...)` returns `AtlasTechniqueResponse`, etc. You never need to write the type annotation; IDEs (VSCode/PyCharm) infer it from the method signature and offer autocomplete on response keys. Import from `contrastapi.models` only if you want to annotate explicitly or pass responses across function boundaries.
|
|
65
|
+
|
|
66
|
+
### Async
|
|
67
|
+
|
|
68
|
+
```python
|
|
69
|
+
import asyncio
|
|
70
|
+
from contrastapi import AsyncContrastAPI
|
|
71
|
+
|
|
72
|
+
async def main():
|
|
73
|
+
async with AsyncContrastAPI(api_key="cc_...") as client:
|
|
74
|
+
defenses = await client.d3fend.defense_for_attack("T1059")
|
|
75
|
+
print(len(defenses["defenses"]))
|
|
76
|
+
|
|
77
|
+
asyncio.run(main())
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### Shortcuts (multi-call helpers)
|
|
81
|
+
|
|
82
|
+
```python
|
|
83
|
+
from contrastapi import ContrastAPI, audit_full, enrich_batch, triage_ioc
|
|
84
|
+
|
|
85
|
+
with ContrastAPI() as client:
|
|
86
|
+
# Auto-route IP/hash/domain to the right enrichment leg
|
|
87
|
+
report = triage_ioc(client, "8.8.8.8") # ioc + threat_report
|
|
88
|
+
|
|
89
|
+
# Audit + subdomains + tech + per-subdomain SSL (capped)
|
|
90
|
+
audit = audit_full(client, "example.com", ssl_subdomains=5)
|
|
91
|
+
|
|
92
|
+
# Auto-detect CVE vs IOC and bulk-route
|
|
93
|
+
enriched = enrich_batch(client, ["CVE-2021-44228", "8.8.8.8", "evil.com"])
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Shortcuts swallow per-leg `ContrastAPIError` so partial failures still return whatever succeeded — see `result["errors"]` for the failure map.
|
|
97
|
+
|
|
98
|
+
## Authentication
|
|
99
|
+
|
|
100
|
+
Pass an API key as the first positional argument or as `api_key=`:
|
|
101
|
+
|
|
102
|
+
```python
|
|
103
|
+
client = ContrastAPI("cc_<your-key>") # 1000 req/hr (Pro tier)
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Get a key at [contrastcyber.com/pricing](https://contrastcyber.com/pricing).
|
|
107
|
+
|
|
108
|
+
## Exception model
|
|
109
|
+
|
|
110
|
+
The SDK maps server error codes (v1.22.2+ wire envelope) to typed exceptions:
|
|
111
|
+
|
|
112
|
+
| Exception | Status | Server code |
|
|
113
|
+
| --- | --- | --- |
|
|
114
|
+
| `InvalidArgumentError` | 400, 422 | `invalid_argument` |
|
|
115
|
+
| `AuthRequiredError` | 401 | `auth_required` |
|
|
116
|
+
| `TierLimitError` | 403 | `tier_limit` |
|
|
117
|
+
| `NotFoundError` | 404 | `not_found` |
|
|
118
|
+
| `RateLimitError` | 429 | `rate_limit_exceeded` |
|
|
119
|
+
| `UpstreamError` | 502 | `upstream_error` |
|
|
120
|
+
| `UpstreamTimeoutError` | 504 | `upstream_timeout` |
|
|
121
|
+
| `TransportError` | n/a | (network failure, before HTTP) |
|
|
122
|
+
| `ContrastAPIError` | * | base / unknown |
|
|
123
|
+
|
|
124
|
+
Every exception carries the parsed envelope:
|
|
125
|
+
|
|
126
|
+
```python
|
|
127
|
+
from contrastapi import ContrastAPI, RateLimitError
|
|
128
|
+
|
|
129
|
+
try:
|
|
130
|
+
client.cve.lookup("CVE-2021-44228")
|
|
131
|
+
except RateLimitError as exc:
|
|
132
|
+
print(exc.message) # "Hourly limit reached"
|
|
133
|
+
print(exc.retry_after_seconds) # 60 (capped at 3600)
|
|
134
|
+
print(exc.upgrade_url) # "https://contrastcyber.com/pricing"
|
|
135
|
+
print(exc.extras) # back-compat top-level fields (tier, limit, ...)
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
## Namespaces
|
|
139
|
+
|
|
140
|
+
| Namespace | Methods |
|
|
141
|
+
| --- | --- |
|
|
142
|
+
| `cve` | `lookup`, `search`, `leading`, `kev`, `exploit`, `bulk` |
|
|
143
|
+
| `cwe` | `lookup` |
|
|
144
|
+
| `ioc` | `lookup`, `hash`, `phishing`, `bulk` |
|
|
145
|
+
| `atlas` | `technique`, `technique_search`, `bulk_technique_lookup`, `case_study`, `case_study_search` |
|
|
146
|
+
| `d3fend` | `defense`, `defense_search`, `defense_for_attack`, `coverage` |
|
|
147
|
+
| `domain` | `report`, `dns`, `whois`, `subdomains`, `certs`, `ssl`, `tech`, `threat`, `monitor`, `vulns`, `audit`, `wayback`, `bulk` |
|
|
148
|
+
| `ip` | `lookup`, `threat_report` |
|
|
149
|
+
| `asn` | `lookup` |
|
|
150
|
+
| `email` | `mx`, `disposable` |
|
|
151
|
+
| `phone` | `lookup` |
|
|
152
|
+
| `password` | `check` (k-anonymity SHA-1 prefix) |
|
|
153
|
+
| `username` | `lookup` |
|
|
154
|
+
| `check` | `secrets`, `injection`, `headers`, `dependencies` |
|
|
155
|
+
| `scan` | `headers` (live HTTP scan) |
|
|
156
|
+
|
|
157
|
+
The async client (`AsyncContrastAPI`) exposes the same namespace surface 1:1 — every method is `async def`.
|
|
158
|
+
|
|
159
|
+
## Parity with the Node SDK
|
|
160
|
+
|
|
161
|
+
| Surface | Node SDK | Python SDK |
|
|
162
|
+
| --- | --- | --- |
|
|
163
|
+
| Sync | ✅ (Promise-based) | ✅ (`ContrastAPI`) |
|
|
164
|
+
| Async | (Promise model) | ✅ (`AsyncContrastAPI`) |
|
|
165
|
+
| Namespace count | 13 | 14 (adds `username`) |
|
|
166
|
+
| `bulk_technique_lookup` (ATLAS) | (added in v1.4.0) | ✅ |
|
|
167
|
+
| `wayback` archive lookup | (added in v1.4.0) | ✅ |
|
|
168
|
+
| Typed errors | `Error` subclasses | full hierarchy with envelope fields |
|
|
169
|
+
| Shortcuts | — | ✅ `triage_ioc`, `audit_full`, `enrich_batch` |
|
|
170
|
+
| Response models | `Promise<any>` | ✅ `TypedDict` (IDE autocomplete, no runtime cost) |
|
|
171
|
+
|
|
172
|
+
## Configuration
|
|
173
|
+
|
|
174
|
+
```python
|
|
175
|
+
client = ContrastAPI(
|
|
176
|
+
api_key="cc_...", # optional; keyless = free tier
|
|
177
|
+
base_url="https://api.contrastcyber.com", # override for self-host
|
|
178
|
+
timeout=30.0, # seconds; clamped to [1, 120]
|
|
179
|
+
allow_insecure=False, # set True to allow http:// (dev only)
|
|
180
|
+
)
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
The transport hard-caps response bodies at 10 MB, sends a `User-Agent: contrastapi-python/<version>` header, and refuses to send your API key over plaintext HTTP even when `allow_insecure=True`.
|
|
184
|
+
|
|
185
|
+
## Links
|
|
186
|
+
|
|
187
|
+
- API docs: https://api.contrastcyber.com/docs
|
|
188
|
+
- Pricing & API key: https://contrastcyber.com/pricing
|
|
189
|
+
- Source: https://github.com/UPinar/contrastapi
|
|
190
|
+
- Issues: https://github.com/UPinar/contrastapi/issues
|
|
191
|
+
|
|
192
|
+
## License
|
|
193
|
+
|
|
194
|
+
MIT — see [LICENSE](../../LICENSE).
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
contrastapi/__init__.py,sha256=gqO2vZ1BJUh7p8nKHxEZk-K-DnY14ae2OUtS5vvyOUg,853
|
|
2
|
+
contrastapi/_transport.py,sha256=B1XylHEm3irTph0_ifUKLUq_rgHuiXwgMgb5ajaBlec,6384
|
|
3
|
+
contrastapi/_version.py,sha256=mumhYmLDqKimAeCXo3ds24UHb0h9ALM2B7GrhoFegAI,23
|
|
4
|
+
contrastapi/client.py,sha256=bpon6dHpoQ3SLCi9NmhUuVENNEeB5bfjMBhZVvYJMzo,5513
|
|
5
|
+
contrastapi/exceptions.py,sha256=VYsAiL1_aEzrdTiToGcO6a6VMQZ_8twbUrWwmrFyNwo,5881
|
|
6
|
+
contrastapi/models.py,sha256=sbu3aQ_QwYrgHmNw_G3eB9qWW0f0lwkIe6Bmiw65ANs,14892
|
|
7
|
+
contrastapi/shortcuts.py,sha256=edUnGiLg3xieZswX3SN1agJtNkxqbEg5sJ5MWVihX8w,6319
|
|
8
|
+
contrastapi/namespaces/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
9
|
+
contrastapi/namespaces/atlas.py,sha256=vsVwxwKtSuB9UZj2RkAAHhxfaGzriDclnD79tsV8AgM,4664
|
|
10
|
+
contrastapi/namespaces/cve.py,sha256=5-tHpVE1fsegvQ8BKrvAfVT6_BB3k1VJb0fqeoxGuZA,3841
|
|
11
|
+
contrastapi/namespaces/d3fend.py,sha256=qsr1sdhCbiRYxbUBiGCAyBkcU0poe3iq-miKeWpOWnw,4169
|
|
12
|
+
contrastapi/namespaces/domain.py,sha256=8fh6M3BPSlhApA8np3FH34fcCaY45oXne-LEDLthJOI,4611
|
|
13
|
+
contrastapi/namespaces/intel.py,sha256=FepgCxs2xgm1qAlpdN-3BpEyZEh54VUE4DQ7wQSEO4Q,3955
|
|
14
|
+
contrastapi/namespaces/ioc.py,sha256=gDgfqa5WlvpvPH7bZbCkQ81dexcQY_Ms4NiIXzfhMF4,2196
|
|
15
|
+
contrastapi/namespaces/security.py,sha256=iVi-oSaYbXTlr3vMXJ3k8055aGbljnGgXEtkjilM2vU,3419
|
|
16
|
+
contrastapi-1.22.3.dist-info/METADATA,sha256=-sOOxnwz_gj657nT2Cvltxt6Bxw7Wi-bKA9GeZyEjsc,7533
|
|
17
|
+
contrastapi-1.22.3.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
18
|
+
contrastapi-1.22.3.dist-info/RECORD,,
|