posture 0.2.2__tar.gz → 0.2.4__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.
- {posture-0.2.2 → posture-0.2.4}/PKG-INFO +1 -1
- {posture-0.2.2 → posture-0.2.4}/pyproject.toml +1 -1
- {posture-0.2.2 → posture-0.2.4}/src/posture/__init__.py +1 -1
- {posture-0.2.2 → posture-0.2.4}/src/posture/collectors/mde.py +64 -2
- {posture-0.2.2 → posture-0.2.4}/tests/test_mde.py +84 -0
- {posture-0.2.2 → posture-0.2.4}/tests/test_posture.py +1 -1
- {posture-0.2.2 → posture-0.2.4}/.github/workflows/publish.yml +0 -0
- {posture-0.2.2 → posture-0.2.4}/.gitignore +0 -0
- {posture-0.2.2 → posture-0.2.4}/LICENSE +0 -0
- {posture-0.2.2 → posture-0.2.4}/README.md +0 -0
- {posture-0.2.2 → posture-0.2.4}/scripts/extract_test.py +0 -0
- {posture-0.2.2 → posture-0.2.4}/src/posture/base.py +0 -0
- {posture-0.2.2 → posture-0.2.4}/src/posture/collectors/__init__.py +0 -0
- {posture-0.2.2 → posture-0.2.4}/src/posture/collectors/_azure_oauth.py +0 -0
- {posture-0.2.2 → posture-0.2.4}/src/posture/collectors/azure_entra.py +0 -0
- {posture-0.2.2 → posture-0.2.4}/src/posture/collectors/crowdstrike.py +0 -0
- {posture-0.2.2 → posture-0.2.4}/src/posture/collectors/intune.py +0 -0
- {posture-0.2.2 → posture-0.2.4}/src/posture/collectors/jamf.py +0 -0
- {posture-0.2.2 → posture-0.2.4}/src/posture/collectors/knowbe4.py +0 -0
- {posture-0.2.2 → posture-0.2.4}/src/posture/collectors/okta.py +0 -0
- {posture-0.2.2 → posture-0.2.4}/src/posture/collectors/salesforce.json +0 -0
- {posture-0.2.2 → posture-0.2.4}/src/posture/collectors/salesforce.py +0 -0
- {posture-0.2.2 → posture-0.2.4}/src/posture/collectors/tenableio.py +0 -0
- {posture-0.2.2 → posture-0.2.4}/src/posture/collectors/upguard.py +0 -0
- {posture-0.2.2 → posture-0.2.4}/src/posture/collectors/workspaceone.py +0 -0
- {posture-0.2.2 → posture-0.2.4}/src/posture/exceptions.py +0 -0
- {posture-0.2.2 → posture-0.2.4}/src/posture/parse.py +0 -0
- {posture-0.2.2 → posture-0.2.4}/tests/fixtures/azure_entra/audit_logs_page.json +0 -0
- {posture-0.2.2 → posture-0.2.4}/tests/fixtures/azure_entra/signins_page.json +0 -0
- {posture-0.2.2 → posture-0.2.4}/tests/fixtures/azure_entra/users_page.json +0 -0
- {posture-0.2.2 → posture-0.2.4}/tests/fixtures/crowdstrike/hosts_bad_timestamps.json +0 -0
- {posture-0.2.2 → posture-0.2.4}/tests/fixtures/crowdstrike/hosts_epoch_timestamps.json +0 -0
- {posture-0.2.2 → posture-0.2.4}/tests/fixtures/crowdstrike/hosts_missing_fields.json +0 -0
- {posture-0.2.2 → posture-0.2.4}/tests/fixtures/crowdstrike/hosts_page.json +0 -0
- {posture-0.2.2 → posture-0.2.4}/tests/fixtures/crowdstrike/vulnerabilities_page.json +0 -0
- {posture-0.2.2 → posture-0.2.4}/tests/fixtures/crowdstrike/zta_page.json +0 -0
- {posture-0.2.2 → posture-0.2.4}/tests/fixtures/intune/attack_simulation_users_page.json +0 -0
- {posture-0.2.2 → posture-0.2.4}/tests/fixtures/intune/attack_simulations_page.json +0 -0
- {posture-0.2.2 → posture-0.2.4}/tests/fixtures/intune/device_configurations_page.json +0 -0
- {posture-0.2.2 → posture-0.2.4}/tests/fixtures/intune/managed_devices_page.json +0 -0
- {posture-0.2.2 → posture-0.2.4}/tests/fixtures/jamf/computers_inventory_detail.json +0 -0
- {posture-0.2.2 → posture-0.2.4}/tests/fixtures/jamf/computers_inventory_page.json +0 -0
- {posture-0.2.2 → posture-0.2.4}/tests/fixtures/jamf/policies_page.json +0 -0
- {posture-0.2.2 → posture-0.2.4}/tests/fixtures/knowbe4/enrollments_page.json +0 -0
- {posture-0.2.2 → posture-0.2.4}/tests/fixtures/knowbe4/pst_recipients_page.json +0 -0
- {posture-0.2.2 → posture-0.2.4}/tests/fixtures/knowbe4/psts_page.json +0 -0
- {posture-0.2.2 → posture-0.2.4}/tests/fixtures/mde/machine_vulnerabilities_page.json +0 -0
- {posture-0.2.2 → posture-0.2.4}/tests/fixtures/mde/machines_page.json +0 -0
- {posture-0.2.2 → posture-0.2.4}/tests/fixtures/mde/vulnerabilities_page.json +0 -0
- {posture-0.2.2 → posture-0.2.4}/tests/fixtures/okta/device_users_page.json +0 -0
- {posture-0.2.2 → posture-0.2.4}/tests/fixtures/okta/devices_page.json +0 -0
- {posture-0.2.2 → posture-0.2.4}/tests/fixtures/okta/users_page.json +0 -0
- {posture-0.2.2 → posture-0.2.4}/tests/fixtures/salesforce/custom_schema.json +0 -0
- {posture-0.2.2 → posture-0.2.4}/tests/fixtures/salesforce/domain_page.json +0 -0
- {posture-0.2.2 → posture-0.2.4}/tests/fixtures/tenableio/assets_page.json +0 -0
- {posture-0.2.2 → posture-0.2.4}/tests/fixtures/tenableio/vulns_page.json +0 -0
- {posture-0.2.2 → posture-0.2.4}/tests/fixtures/upguard/organisation.json +0 -0
- {posture-0.2.2 → posture-0.2.4}/tests/fixtures/upguard/vendor_risks_page.json +0 -0
- {posture-0.2.2 → posture-0.2.4}/tests/fixtures/upguard/vendors_page.json +0 -0
- {posture-0.2.2 → posture-0.2.4}/tests/fixtures/workspaceone/computers_page.json +0 -0
- {posture-0.2.2 → posture-0.2.4}/tests/test_azure_entra.py +0 -0
- {posture-0.2.2 → posture-0.2.4}/tests/test_catalog.py +0 -0
- {posture-0.2.2 → posture-0.2.4}/tests/test_crowdstrike.py +0 -0
- {posture-0.2.2 → posture-0.2.4}/tests/test_intune.py +0 -0
- {posture-0.2.2 → posture-0.2.4}/tests/test_jamf.py +0 -0
- {posture-0.2.2 → posture-0.2.4}/tests/test_knowbe4.py +0 -0
- {posture-0.2.2 → posture-0.2.4}/tests/test_okta.py +0 -0
- {posture-0.2.2 → posture-0.2.4}/tests/test_parse.py +0 -0
- {posture-0.2.2 → posture-0.2.4}/tests/test_parse_azure_entra.py +0 -0
- {posture-0.2.2 → posture-0.2.4}/tests/test_parse_intune.py +0 -0
- {posture-0.2.2 → posture-0.2.4}/tests/test_parse_jamf.py +0 -0
- {posture-0.2.2 → posture-0.2.4}/tests/test_parse_knowbe4.py +0 -0
- {posture-0.2.2 → posture-0.2.4}/tests/test_parse_mde.py +0 -0
- {posture-0.2.2 → posture-0.2.4}/tests/test_parse_okta.py +0 -0
- {posture-0.2.2 → posture-0.2.4}/tests/test_parse_salesforce.py +0 -0
- {posture-0.2.2 → posture-0.2.4}/tests/test_parse_tenableio.py +0 -0
- {posture-0.2.2 → posture-0.2.4}/tests/test_parse_upguard.py +0 -0
- {posture-0.2.2 → posture-0.2.4}/tests/test_parse_workspaceone.py +0 -0
- {posture-0.2.2 → posture-0.2.4}/tests/test_redaction.py +0 -0
- {posture-0.2.2 → posture-0.2.4}/tests/test_salesforce.py +0 -0
- {posture-0.2.2 → posture-0.2.4}/tests/test_tenableio.py +0 -0
- {posture-0.2.2 → posture-0.2.4}/tests/test_upguard.py +0 -0
- {posture-0.2.2 → posture-0.2.4}/tests/test_workspaceone.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: posture
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.4
|
|
4
4
|
Summary: Runtime-agnostic Python library for CCM (Continuous Control Monitoring) data collection.
|
|
5
5
|
Project-URL: Homepage, https://github.com/massyn/posture
|
|
6
6
|
Author-email: Phil Massyn <phil.massyn@icloud.com>
|
|
@@ -33,6 +33,11 @@ each retry just re-bursts at lower concurrency instead of avoiding the 429s
|
|
|
33
33
|
in the first place. This is the same root cause and fix as the KnowBe4
|
|
34
34
|
collector's ``_pace_request``.
|
|
35
35
|
|
|
36
|
+
A 5xx for a single machine (e.g. a stale/decommissioned device record) is
|
|
37
|
+
handled locally per-machine — see ``_fetch_all_vulns_for_machine`` — rather
|
|
38
|
+
than propagating up to base.py's batch-level retry, which would otherwise
|
|
39
|
+
fail (or re-burst) the whole fan-out over one bad machine.
|
|
40
|
+
|
|
36
41
|
Resources: ``machines``, ``vulnerabilities``, ``device_av_info``,
|
|
37
42
|
``machine_vulnerabilities`` (requires machines ids).
|
|
38
43
|
"""
|
|
@@ -40,17 +45,32 @@ Resources: ``machines``, ``vulnerabilities``, ``device_av_info``,
|
|
|
40
45
|
from __future__ import annotations
|
|
41
46
|
|
|
42
47
|
import concurrent.futures
|
|
48
|
+
import logging
|
|
43
49
|
import threading
|
|
44
50
|
import time
|
|
45
51
|
from typing import Any
|
|
46
52
|
|
|
53
|
+
import requests
|
|
54
|
+
|
|
47
55
|
from posture.base import Collector, RateLimitedSignal, UnauthorizedSignal
|
|
48
56
|
from posture.collectors._azure_oauth import fetch_azure_ad_token
|
|
49
57
|
|
|
58
|
+
logger = logging.getLogger("posture.collectors.mde")
|
|
59
|
+
|
|
50
60
|
_API_BASE_URL = "https://api.security.microsoft.com"
|
|
51
61
|
_PAGE_SIZE = 1000 # comfortably under the documented $top max of 8,000
|
|
52
62
|
_DEFAULT_MACHINE_VULN_MAX_WORKERS = 10
|
|
53
63
|
|
|
64
|
+
# A lone machine 500ing is a per-device MDE fault, not a whole-tenant problem
|
|
65
|
+
# (unlike 429/401, which are meaningful at the batch level and handled by
|
|
66
|
+
# base.py's _request_with_retry). Retrying the entire fan-out for one bad
|
|
67
|
+
# machine would burn the shared retry budget and re-halve concurrency for
|
|
68
|
+
# every other machine, so a 5xx for a single machine is retried locally here
|
|
69
|
+
# and, if it still fails, that machine is skipped rather than failing the
|
|
70
|
+
# whole 'machine_vulnerabilities' resource.
|
|
71
|
+
_MACHINE_VULN_SERVER_ERROR_RETRIES = 2
|
|
72
|
+
_MACHINE_VULN_SERVER_ERROR_WAIT_SECONDS = 60.0
|
|
73
|
+
|
|
54
74
|
# MDE's documented general API limit is ~100 calls/minute per app registration.
|
|
55
75
|
# Staying comfortably under that (0.65s between requests ~= 92/min) leaves
|
|
56
76
|
# headroom for other traffic against the same app registration.
|
|
@@ -294,8 +314,50 @@ class MdeCollector(Collector):
|
|
|
294
314
|
# No $top/$skip/nextLink documented for this endpoint — it's a
|
|
295
315
|
# single, complete response (unlike the org-wide list endpoints).
|
|
296
316
|
endpoint = _ENDPOINTS["machine_vulnerabilities"].format(id=machine_id)
|
|
297
|
-
|
|
298
|
-
|
|
317
|
+
attempt = 0
|
|
318
|
+
while True:
|
|
319
|
+
try:
|
|
320
|
+
response = self._get(_API_BASE_URL + endpoint)
|
|
321
|
+
return response.json().get("value", [])
|
|
322
|
+
except requests.exceptions.HTTPError as exc:
|
|
323
|
+
status = exc.response.status_code if exc.response is not None else None
|
|
324
|
+
if status == 404:
|
|
325
|
+
# Machine-specific endpoint: a 404 means this machine has
|
|
326
|
+
# no vulnerability data (e.g. offboarded since the machine
|
|
327
|
+
# list was pulled), not a collection-wide failure.
|
|
328
|
+
logger.info(
|
|
329
|
+
"machine_vulnerabilities: no data for machine (404), skipping",
|
|
330
|
+
extra={
|
|
331
|
+
"source": self.env_prefix.lower(),
|
|
332
|
+
"machine_id": machine_id,
|
|
333
|
+
},
|
|
334
|
+
)
|
|
335
|
+
return []
|
|
336
|
+
if status is None or status < 500:
|
|
337
|
+
raise
|
|
338
|
+
attempt += 1
|
|
339
|
+
if attempt > _MACHINE_VULN_SERVER_ERROR_RETRIES:
|
|
340
|
+
logger.warning(
|
|
341
|
+
"machine_vulnerabilities: skipping machine after repeated "
|
|
342
|
+
"server errors",
|
|
343
|
+
extra={
|
|
344
|
+
"source": self.env_prefix.lower(),
|
|
345
|
+
"machine_id": machine_id,
|
|
346
|
+
"status": status,
|
|
347
|
+
"attempts": attempt,
|
|
348
|
+
},
|
|
349
|
+
)
|
|
350
|
+
return []
|
|
351
|
+
logger.warning(
|
|
352
|
+
"machine_vulnerabilities: server error for machine, retrying",
|
|
353
|
+
extra={
|
|
354
|
+
"source": self.env_prefix.lower(),
|
|
355
|
+
"machine_id": machine_id,
|
|
356
|
+
"status": status,
|
|
357
|
+
"attempt": attempt,
|
|
358
|
+
},
|
|
359
|
+
)
|
|
360
|
+
time.sleep(_MACHINE_VULN_SERVER_ERROR_WAIT_SECONDS)
|
|
299
361
|
|
|
300
362
|
def _pace_request(self) -> None:
|
|
301
363
|
# Shared across threads so concurrent machine_vulnerabilities workers
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import json
|
|
2
|
+
from unittest.mock import patch
|
|
2
3
|
|
|
3
4
|
import responses
|
|
4
5
|
|
|
@@ -139,3 +140,86 @@ def test_machine_vulnerabilities_fans_out_and_injects_machine_id() -> None:
|
|
|
139
140
|
|
|
140
141
|
assert len(df) == 2
|
|
141
142
|
assert set(df["machine_id"]) == {"machine-1", "machine-2"}
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
@responses.activate
|
|
146
|
+
def test_machine_vulnerabilities_retries_then_recovers_from_server_error() -> None:
|
|
147
|
+
responses.add(
|
|
148
|
+
responses.POST,
|
|
149
|
+
"https://login.microsoftonline.com/tenant-1/oauth2/v2.0/token",
|
|
150
|
+
json={"access_token": "tok"},
|
|
151
|
+
status=200,
|
|
152
|
+
)
|
|
153
|
+
responses.add(
|
|
154
|
+
responses.GET,
|
|
155
|
+
"https://api.security.microsoft.com/api/machines",
|
|
156
|
+
json={"value": [{"id": "machine-1"}]},
|
|
157
|
+
status=200,
|
|
158
|
+
)
|
|
159
|
+
responses.add(
|
|
160
|
+
responses.GET,
|
|
161
|
+
"https://api.security.microsoft.com/api/machines/machine-1/vulnerabilities",
|
|
162
|
+
status=500,
|
|
163
|
+
)
|
|
164
|
+
responses.add(
|
|
165
|
+
responses.GET,
|
|
166
|
+
"https://api.security.microsoft.com/api/machines/machine-1/vulnerabilities",
|
|
167
|
+
json={"value": [{"id": "mv-1"}]},
|
|
168
|
+
status=200,
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
ccm = CCM(
|
|
172
|
+
"mde", {"tenant_id": "tenant-1", "client_id": "id", "client_secret": "secret"}
|
|
173
|
+
)
|
|
174
|
+
with patch("posture.collectors.mde.time.sleep") as mock_sleep:
|
|
175
|
+
df = ccm.collect("machine_vulnerabilities")
|
|
176
|
+
|
|
177
|
+
assert len(df) == 1
|
|
178
|
+
assert 60.0 in [call.args[0] for call in mock_sleep.call_args_list]
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
@responses.activate
|
|
182
|
+
def test_machine_vulnerabilities_skips_machine_after_repeated_server_errors() -> None:
|
|
183
|
+
responses.add(
|
|
184
|
+
responses.POST,
|
|
185
|
+
"https://login.microsoftonline.com/tenant-1/oauth2/v2.0/token",
|
|
186
|
+
json={"access_token": "tok"},
|
|
187
|
+
status=200,
|
|
188
|
+
)
|
|
189
|
+
responses.add(
|
|
190
|
+
responses.GET,
|
|
191
|
+
"https://api.security.microsoft.com/api/machines",
|
|
192
|
+
json={"value": [{"id": "machine-1"}, {"id": "machine-2"}]},
|
|
193
|
+
status=200,
|
|
194
|
+
)
|
|
195
|
+
responses.add(
|
|
196
|
+
responses.GET,
|
|
197
|
+
"https://api.security.microsoft.com/api/machines/machine-1/vulnerabilities",
|
|
198
|
+
status=500,
|
|
199
|
+
)
|
|
200
|
+
responses.add(
|
|
201
|
+
responses.GET,
|
|
202
|
+
"https://api.security.microsoft.com/api/machines/machine-1/vulnerabilities",
|
|
203
|
+
status=500,
|
|
204
|
+
)
|
|
205
|
+
responses.add(
|
|
206
|
+
responses.GET,
|
|
207
|
+
"https://api.security.microsoft.com/api/machines/machine-1/vulnerabilities",
|
|
208
|
+
status=500,
|
|
209
|
+
)
|
|
210
|
+
responses.add(
|
|
211
|
+
responses.GET,
|
|
212
|
+
"https://api.security.microsoft.com/api/machines/machine-2/vulnerabilities",
|
|
213
|
+
json={"value": [{"id": "mv-2"}]},
|
|
214
|
+
status=200,
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
ccm = CCM(
|
|
218
|
+
"mde", {"tenant_id": "tenant-1", "client_id": "id", "client_secret": "secret"}
|
|
219
|
+
)
|
|
220
|
+
with patch("posture.collectors.mde.time.sleep"):
|
|
221
|
+
df = ccm.collect("machine_vulnerabilities")
|
|
222
|
+
|
|
223
|
+
# machine-1 is skipped after exhausting retries; machine-2 still succeeds.
|
|
224
|
+
assert len(df) == 1
|
|
225
|
+
assert df.iloc[0]["machine_id"] == "machine-2"
|
|
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
|
|
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
|
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|