contrastapi 1.22.4__tar.gz → 1.22.5__tar.gz
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-1.22.4 → contrastapi-1.22.5}/.gitignore +9 -0
- {contrastapi-1.22.4 → contrastapi-1.22.5}/PKG-INFO +1 -1
- contrastapi-1.22.5/contrastapi/_version.py +1 -0
- {contrastapi-1.22.4 → contrastapi-1.22.5}/contrastapi/exceptions.py +2 -1
- {contrastapi-1.22.4 → contrastapi-1.22.5}/contrastapi/models.py +57 -0
- {contrastapi-1.22.4 → contrastapi-1.22.5}/contrastapi/namespaces/cve.py +18 -0
- {contrastapi-1.22.4 → contrastapi-1.22.5}/pyproject.toml +1 -1
- {contrastapi-1.22.4 → contrastapi-1.22.5}/tests/test_namespaces.py +2 -2
- {contrastapi-1.22.4 → contrastapi-1.22.5}/tests/test_transport.py +2 -2
- contrastapi-1.22.4/contrastapi/_version.py +0 -1
- {contrastapi-1.22.4 → contrastapi-1.22.5}/README.md +0 -0
- {contrastapi-1.22.4 → contrastapi-1.22.5}/contrastapi/__init__.py +0 -0
- {contrastapi-1.22.4 → contrastapi-1.22.5}/contrastapi/_transport.py +0 -0
- {contrastapi-1.22.4 → contrastapi-1.22.5}/contrastapi/client.py +0 -0
- {contrastapi-1.22.4 → contrastapi-1.22.5}/contrastapi/namespaces/__init__.py +0 -0
- {contrastapi-1.22.4 → contrastapi-1.22.5}/contrastapi/namespaces/atlas.py +0 -0
- {contrastapi-1.22.4 → contrastapi-1.22.5}/contrastapi/namespaces/d3fend.py +0 -0
- {contrastapi-1.22.4 → contrastapi-1.22.5}/contrastapi/namespaces/domain.py +0 -0
- {contrastapi-1.22.4 → contrastapi-1.22.5}/contrastapi/namespaces/intel.py +0 -0
- {contrastapi-1.22.4 → contrastapi-1.22.5}/contrastapi/namespaces/ioc.py +0 -0
- {contrastapi-1.22.4 → contrastapi-1.22.5}/contrastapi/namespaces/security.py +0 -0
- {contrastapi-1.22.4 → contrastapi-1.22.5}/contrastapi/shortcuts.py +0 -0
- {contrastapi-1.22.4 → contrastapi-1.22.5}/tests/__init__.py +0 -0
- {contrastapi-1.22.4 → contrastapi-1.22.5}/tests/conftest.py +0 -0
- {contrastapi-1.22.4 → contrastapi-1.22.5}/tests/test_remaining_namespaces.py +0 -0
- {contrastapi-1.22.4 → contrastapi-1.22.5}/tests/test_review_fixes.py +0 -0
- {contrastapi-1.22.4 → contrastapi-1.22.5}/tests/test_review_round2_fixes.py +0 -0
- {contrastapi-1.22.4 → contrastapi-1.22.5}/tests/test_shortcuts.py +0 -0
|
@@ -18,3 +18,12 @@ research_landing*.md
|
|
|
18
18
|
.coverage
|
|
19
19
|
deploy/
|
|
20
20
|
.aider*
|
|
21
|
+
|
|
22
|
+
# SDK build artifacts (regenerated per release)
|
|
23
|
+
sdk/python/dist/
|
|
24
|
+
|
|
25
|
+
sdk/node/publish_v*.txt
|
|
26
|
+
sdk/python/publish_v*.txt
|
|
27
|
+
|
|
28
|
+
# One-shot CLI verification reports
|
|
29
|
+
cli_prompt_verify.md
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: contrastapi
|
|
3
|
-
Version: 1.22.
|
|
3
|
+
Version: 1.22.5
|
|
4
4
|
Summary: Official Python SDK for ContrastAPI — security intelligence for developers and AI agents
|
|
5
5
|
Project-URL: Homepage, https://contrastcyber.com
|
|
6
6
|
Project-URL: Documentation, https://api.contrastcyber.com/docs
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "1.22.5"
|
|
@@ -8,6 +8,7 @@ the parsed `ErrorDetail` envelope (dict) from the wire — `.code`, `.message`,
|
|
|
8
8
|
|
|
9
9
|
from __future__ import annotations
|
|
10
10
|
|
|
11
|
+
import math
|
|
11
12
|
from typing import Any
|
|
12
13
|
|
|
13
14
|
# Wire-boundary defensive caps mirror server-side `_ERROR_MESSAGE_MAX_LEN` /
|
|
@@ -67,7 +68,7 @@ def _coerce_retry_after(value: Any) -> int | None:
|
|
|
67
68
|
return None
|
|
68
69
|
# Reject NaN — `int(float('nan'))` raises ValueError but inf raises
|
|
69
70
|
# OverflowError; both must be silently dropped to None per defensive policy.
|
|
70
|
-
if isinstance(value, float) and (value
|
|
71
|
+
if isinstance(value, float) and (math.isnan(value) or math.isinf(value)):
|
|
71
72
|
return None
|
|
72
73
|
try:
|
|
73
74
|
as_int = int(value)
|
|
@@ -70,6 +70,9 @@ class CveResponse(TypedDict, total=False):
|
|
|
70
70
|
cvss_v3: float
|
|
71
71
|
cvss_breakdown: dict[str, Any]
|
|
72
72
|
cwe_id: str
|
|
73
|
+
cwes: list[str] # v1.28.0 (B4A) — all CWE identifiers, Primary first
|
|
74
|
+
vulnerability_status: str # v1.29.0 (B5) — NVD lifecycle (Analyzed/Modified/Rejected/...)
|
|
75
|
+
cve_tags: list[str] # v1.29.0 (B5) — NVD cveTags (e.g. 'disputed')
|
|
73
76
|
epss: dict[str, Any]
|
|
74
77
|
kev: dict[str, Any]
|
|
75
78
|
affected_products: list[dict[str, Any]]
|
|
@@ -78,9 +81,16 @@ class CveResponse(TypedDict, total=False):
|
|
|
78
81
|
modified: str
|
|
79
82
|
references: list[str]
|
|
80
83
|
total_references: int
|
|
84
|
+
total_references_unique: int # v1.29.0 (B6A) — unique-URL count from refs_with_tags
|
|
85
|
+
references_full: list[dict[str, Any]] # v1.29.0 (B6B) — [{url, tags, source}] when ?include_reference_tags=true
|
|
81
86
|
patch_available: bool
|
|
82
87
|
patch_url: str
|
|
83
88
|
sources: list[str]
|
|
89
|
+
cvss_v2: float # v1.29.0 (B7) — CVSS v2.0 base score (always emitted)
|
|
90
|
+
cvss_v2_vector: str # v1.29.0 (B7) — CVSS v2.0 vector string
|
|
91
|
+
severity_sources: list[dict[str, Any]] # v1.29.0 (B7) — per-source severity breakdown
|
|
92
|
+
severity_consensus: str # v1.29.0 (B7) — majority-bucket consensus
|
|
93
|
+
severity_disagreement: bool # v1.29.0 (B7) — true when sources disagree
|
|
84
94
|
verdict: Verdict
|
|
85
95
|
next_calls: list[PivotHint]
|
|
86
96
|
|
|
@@ -103,6 +113,8 @@ class KevDetailResponse(TypedDict, total=False):
|
|
|
103
113
|
cve_id: str
|
|
104
114
|
in_kev: bool
|
|
105
115
|
date_added: str
|
|
116
|
+
date_updated: str # v1.29.0 (B5) — feed update timestamp
|
|
117
|
+
date_removed: str # v1.29.0 (B5) — soft-delete lifecycle (null when still active)
|
|
106
118
|
due_date: str
|
|
107
119
|
required_action: str
|
|
108
120
|
known_ransomware_use: bool
|
|
@@ -147,6 +159,51 @@ class ExploitResponse(TypedDict, total=False):
|
|
|
147
159
|
next_calls: list[PivotHint]
|
|
148
160
|
|
|
149
161
|
|
|
162
|
+
# --- v1.29.1: composite risk score + CVSS v3.x parser ---
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
class CvssMetrics(TypedDict, total=False):
|
|
166
|
+
attack_vector: str
|
|
167
|
+
attack_complexity: str
|
|
168
|
+
privileges_required: str
|
|
169
|
+
user_interaction: str
|
|
170
|
+
scope: str
|
|
171
|
+
confidentiality_impact: str
|
|
172
|
+
integrity_impact: str
|
|
173
|
+
availability_impact: str
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
class CvssDetailsResponse(TypedDict, total=False):
|
|
177
|
+
"""v1.29.1 — `GET /v1/cvss/details?vector=...`."""
|
|
178
|
+
|
|
179
|
+
version: str
|
|
180
|
+
vector: str
|
|
181
|
+
base_score: float
|
|
182
|
+
base_severity: str
|
|
183
|
+
metrics: CvssMetrics
|
|
184
|
+
temporal_score: float | None
|
|
185
|
+
environmental_score: float | None
|
|
186
|
+
summary: str
|
|
187
|
+
verdict: Verdict
|
|
188
|
+
next_calls: list[PivotHint]
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
class RiskScoreResponse(TypedDict, total=False):
|
|
192
|
+
"""v1.29.1 — `GET /v1/cve/{cve_id}/risk_score`."""
|
|
193
|
+
|
|
194
|
+
cve_id: str
|
|
195
|
+
score: float
|
|
196
|
+
label: str # CRITICAL / HIGH / MEDIUM / LOW
|
|
197
|
+
urgency: str
|
|
198
|
+
has_public_poc: bool
|
|
199
|
+
components: dict[str, Any]
|
|
200
|
+
boosters_applied: list[str]
|
|
201
|
+
recommendation: str
|
|
202
|
+
summary: str
|
|
203
|
+
verdict: Verdict
|
|
204
|
+
next_calls: list[PivotHint]
|
|
205
|
+
|
|
206
|
+
|
|
150
207
|
# --- Bulk parity (v1.21.0+ unified status enum: ok/error/not_found/invalid_format) ---
|
|
151
208
|
|
|
152
209
|
|
|
@@ -10,9 +10,11 @@ from ..models import (
|
|
|
10
10
|
BulkCveResponse,
|
|
11
11
|
CveResponse,
|
|
12
12
|
CveSearchResponse,
|
|
13
|
+
CvssDetailsResponse,
|
|
13
14
|
CweLookupResponse,
|
|
14
15
|
ExploitResponse,
|
|
15
16
|
KevDetailResponse,
|
|
17
|
+
RiskScoreResponse,
|
|
16
18
|
)
|
|
17
19
|
|
|
18
20
|
|
|
@@ -82,6 +84,14 @@ class Cve:
|
|
|
82
84
|
def exploit(self, cve_id: str) -> ExploitResponse:
|
|
83
85
|
return self._t.get(f"/v1/exploit/{_enc(cve_id)}")
|
|
84
86
|
|
|
87
|
+
def risk_score(self, cve_id: str) -> RiskScoreResponse:
|
|
88
|
+
"""v1.29.1 — Composite risk score (CVSS+EPSS+KEV+PoC fusion, 0-100)."""
|
|
89
|
+
return self._t.get(f"/v1/cve/{_enc(cve_id)}/risk_score")
|
|
90
|
+
|
|
91
|
+
def cvss_details(self, vector: str) -> CvssDetailsResponse:
|
|
92
|
+
"""v1.29.1 — Parse a CVSS v3.x vector into per-metric breakdown + recomputed score."""
|
|
93
|
+
return self._t.get("/v1/cvss/details", params={"vector": vector})
|
|
94
|
+
|
|
85
95
|
def bulk(self, cve_ids: list[str]) -> BulkCveResponse:
|
|
86
96
|
return self._t.post("/v1/cves/bulk", json_body={"cve_ids": _validate_bulk_ids(cve_ids)})
|
|
87
97
|
|
|
@@ -113,6 +123,14 @@ class AsyncCve:
|
|
|
113
123
|
async def exploit(self, cve_id: str) -> ExploitResponse:
|
|
114
124
|
return await self._t.aget(f"/v1/exploit/{_enc(cve_id)}")
|
|
115
125
|
|
|
126
|
+
async def risk_score(self, cve_id: str) -> RiskScoreResponse:
|
|
127
|
+
"""v1.29.1 — Composite risk score (CVSS+EPSS+KEV+PoC fusion, 0-100)."""
|
|
128
|
+
return await self._t.aget(f"/v1/cve/{_enc(cve_id)}/risk_score")
|
|
129
|
+
|
|
130
|
+
async def cvss_details(self, vector: str) -> CvssDetailsResponse:
|
|
131
|
+
"""v1.29.1 — Parse a CVSS v3.x vector into per-metric breakdown + recomputed score."""
|
|
132
|
+
return await self._t.aget("/v1/cvss/details", params={"vector": vector})
|
|
133
|
+
|
|
116
134
|
async def bulk(self, cve_ids: list[str]) -> BulkCveResponse:
|
|
117
135
|
return await self._t.apost("/v1/cves/bulk", json_body={"cve_ids": _validate_bulk_ids(cve_ids)})
|
|
118
136
|
|
|
@@ -302,7 +302,7 @@ def test_d3fend_coverage_validation():
|
|
|
302
302
|
@respx.mock
|
|
303
303
|
def test_status_endpoint():
|
|
304
304
|
route = respx.get("https://api.contrastcyber.com/v1/status").mock(
|
|
305
|
-
return_value=httpx.Response(200, json={"status": "ok", "version": "1.22.
|
|
305
|
+
return_value=httpx.Response(200, json={"status": "ok", "version": "1.22.5"})
|
|
306
306
|
)
|
|
307
307
|
with ContrastAPI() as client:
|
|
308
308
|
result = client.status()
|
|
@@ -322,4 +322,4 @@ def test_usage_endpoint():
|
|
|
322
322
|
|
|
323
323
|
def test_client_exposes_version_string():
|
|
324
324
|
with ContrastAPI() as client:
|
|
325
|
-
assert client.version == "1.22.
|
|
325
|
+
assert client.version == "1.22.5"
|
|
@@ -22,7 +22,7 @@ from contrastapi import (
|
|
|
22
22
|
|
|
23
23
|
|
|
24
24
|
def test_version_is_pinned():
|
|
25
|
-
assert __version__ == "1.22.
|
|
25
|
+
assert __version__ == "1.22.5"
|
|
26
26
|
|
|
27
27
|
|
|
28
28
|
def test_default_base_url_is_https():
|
|
@@ -70,7 +70,7 @@ def test_get_returns_json_body():
|
|
|
70
70
|
@respx.mock
|
|
71
71
|
def test_user_agent_header_sent():
|
|
72
72
|
route = respx.get("https://api.contrastcyber.com/v1/status").mock(
|
|
73
|
-
return_value=httpx.Response(200, json={"status": "ok", "version": "1.22.
|
|
73
|
+
return_value=httpx.Response(200, json={"status": "ok", "version": "1.22.5"})
|
|
74
74
|
)
|
|
75
75
|
with ContrastAPI() as client:
|
|
76
76
|
client.status()
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "1.22.3"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|