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.
Files changed (28) hide show
  1. {contrastapi-1.22.4 → contrastapi-1.22.5}/.gitignore +9 -0
  2. {contrastapi-1.22.4 → contrastapi-1.22.5}/PKG-INFO +1 -1
  3. contrastapi-1.22.5/contrastapi/_version.py +1 -0
  4. {contrastapi-1.22.4 → contrastapi-1.22.5}/contrastapi/exceptions.py +2 -1
  5. {contrastapi-1.22.4 → contrastapi-1.22.5}/contrastapi/models.py +57 -0
  6. {contrastapi-1.22.4 → contrastapi-1.22.5}/contrastapi/namespaces/cve.py +18 -0
  7. {contrastapi-1.22.4 → contrastapi-1.22.5}/pyproject.toml +1 -1
  8. {contrastapi-1.22.4 → contrastapi-1.22.5}/tests/test_namespaces.py +2 -2
  9. {contrastapi-1.22.4 → contrastapi-1.22.5}/tests/test_transport.py +2 -2
  10. contrastapi-1.22.4/contrastapi/_version.py +0 -1
  11. {contrastapi-1.22.4 → contrastapi-1.22.5}/README.md +0 -0
  12. {contrastapi-1.22.4 → contrastapi-1.22.5}/contrastapi/__init__.py +0 -0
  13. {contrastapi-1.22.4 → contrastapi-1.22.5}/contrastapi/_transport.py +0 -0
  14. {contrastapi-1.22.4 → contrastapi-1.22.5}/contrastapi/client.py +0 -0
  15. {contrastapi-1.22.4 → contrastapi-1.22.5}/contrastapi/namespaces/__init__.py +0 -0
  16. {contrastapi-1.22.4 → contrastapi-1.22.5}/contrastapi/namespaces/atlas.py +0 -0
  17. {contrastapi-1.22.4 → contrastapi-1.22.5}/contrastapi/namespaces/d3fend.py +0 -0
  18. {contrastapi-1.22.4 → contrastapi-1.22.5}/contrastapi/namespaces/domain.py +0 -0
  19. {contrastapi-1.22.4 → contrastapi-1.22.5}/contrastapi/namespaces/intel.py +0 -0
  20. {contrastapi-1.22.4 → contrastapi-1.22.5}/contrastapi/namespaces/ioc.py +0 -0
  21. {contrastapi-1.22.4 → contrastapi-1.22.5}/contrastapi/namespaces/security.py +0 -0
  22. {contrastapi-1.22.4 → contrastapi-1.22.5}/contrastapi/shortcuts.py +0 -0
  23. {contrastapi-1.22.4 → contrastapi-1.22.5}/tests/__init__.py +0 -0
  24. {contrastapi-1.22.4 → contrastapi-1.22.5}/tests/conftest.py +0 -0
  25. {contrastapi-1.22.4 → contrastapi-1.22.5}/tests/test_remaining_namespaces.py +0 -0
  26. {contrastapi-1.22.4 → contrastapi-1.22.5}/tests/test_review_fixes.py +0 -0
  27. {contrastapi-1.22.4 → contrastapi-1.22.5}/tests/test_review_round2_fixes.py +0 -0
  28. {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.4
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 != value or value in (float("inf"), float("-inf"))):
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
 
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "contrastapi"
7
- version = "1.22.4"
7
+ version = "1.22.5"
8
8
  description = "Official Python SDK for ContrastAPI — security intelligence for developers and AI agents"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -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.3"})
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.3"
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.3"
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.3"})
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