check-opencloud-security 1.2.2__tar.gz → 1.2.3__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.
- {check_opencloud_security-1.2.2 → check_opencloud_security-1.2.3}/CHANGELOG.md +8 -0
- {check_opencloud_security-1.2.2 → check_opencloud_security-1.2.3}/PKG-INFO +6 -1
- {check_opencloud_security-1.2.2 → check_opencloud_security-1.2.3}/README.md +5 -0
- {check_opencloud_security-1.2.2 → check_opencloud_security-1.2.3}/check_opencloud_security.py +42 -0
- {check_opencloud_security-1.2.2 → check_opencloud_security-1.2.3}/opencloud_local_scan/wizard.py +13 -0
- {check_opencloud_security-1.2.2 → check_opencloud_security-1.2.3}/pyproject.toml +1 -1
- {check_opencloud_security-1.2.2 → check_opencloud_security-1.2.3}/tests/test_env_config.py +5 -0
- {check_opencloud_security-1.2.2 → check_opencloud_security-1.2.3}/tests/test_webhook.py +67 -1
- {check_opencloud_security-1.2.2 → check_opencloud_security-1.2.3}/.gitignore +0 -0
- {check_opencloud_security-1.2.2 → check_opencloud_security-1.2.3}/LICENSE +0 -0
- {check_opencloud_security-1.2.2 → check_opencloud_security-1.2.3}/ansible/README.md +0 -0
- {check_opencloud_security-1.2.2 → check_opencloud_security-1.2.3}/contrib/README.md +0 -0
- {check_opencloud_security-1.2.2 → check_opencloud_security-1.2.3}/docs/README.md +0 -0
- {check_opencloud_security-1.2.2 → check_opencloud_security-1.2.3}/opencloud_local_scan/README.md +0 -0
- {check_opencloud_security-1.2.2 → check_opencloud_security-1.2.3}/opencloud_local_scan/__init__.py +0 -0
- {check_opencloud_security-1.2.2 → check_opencloud_security-1.2.3}/opencloud_local_scan/baseline.py +0 -0
- {check_opencloud_security-1.2.2 → check_opencloud_security-1.2.3}/opencloud_local_scan/cli.py +0 -0
- {check_opencloud_security-1.2.2 → check_opencloud_security-1.2.3}/opencloud_local_scan/completion.py +0 -0
- {check_opencloud_security-1.2.2 → check_opencloud_security-1.2.3}/opencloud_local_scan/config.py +0 -0
- {check_opencloud_security-1.2.2 → check_opencloud_security-1.2.3}/opencloud_local_scan/data/release_schedule.json +0 -0
- {check_opencloud_security-1.2.2 → check_opencloud_security-1.2.3}/opencloud_local_scan/data/vulnerabilities.json +0 -0
- {check_opencloud_security-1.2.2 → check_opencloud_security-1.2.3}/opencloud_local_scan/factory.py +0 -0
- {check_opencloud_security-1.2.2 → check_opencloud_security-1.2.3}/opencloud_local_scan/hardening.py +0 -0
- {check_opencloud_security-1.2.2 → check_opencloud_security-1.2.3}/opencloud_local_scan/releases.py +0 -0
- {check_opencloud_security-1.2.2 → check_opencloud_security-1.2.3}/opencloud_local_scan/scanner.py +0 -0
- {check_opencloud_security-1.2.2 → check_opencloud_security-1.2.3}/opencloud_local_scan/secrets.py +0 -0
- {check_opencloud_security-1.2.2 → check_opencloud_security-1.2.3}/opencloud_local_scan/selfupdate.py +0 -0
- {check_opencloud_security-1.2.2 → check_opencloud_security-1.2.3}/opencloud_local_scan/service.py +0 -0
- {check_opencloud_security-1.2.2 → check_opencloud_security-1.2.3}/opencloud_local_scan/versions.py +0 -0
- {check_opencloud_security-1.2.2 → check_opencloud_security-1.2.3}/opencloud_local_scan/vulndb.py +0 -0
- {check_opencloud_security-1.2.2 → check_opencloud_security-1.2.3}/secrets/README.md +0 -0
- {check_opencloud_security-1.2.2 → check_opencloud_security-1.2.3}/tests/__init__.py +0 -0
- {check_opencloud_security-1.2.2 → check_opencloud_security-1.2.3}/tests/conftest.py +0 -0
- {check_opencloud_security-1.2.2 → check_opencloud_security-1.2.3}/tests/fake_opencloud.py +0 -0
- {check_opencloud_security-1.2.2 → check_opencloud_security-1.2.3}/tests/test_baseline.py +0 -0
- {check_opencloud_security-1.2.2 → check_opencloud_security-1.2.3}/tests/test_completion.py +0 -0
- {check_opencloud_security-1.2.2 → check_opencloud_security-1.2.3}/tests/test_concurrency.py +0 -0
- {check_opencloud_security-1.2.2 → check_opencloud_security-1.2.3}/tests/test_config.py +0 -0
- {check_opencloud_security-1.2.2 → check_opencloud_security-1.2.3}/tests/test_e2e_cli.py +0 -0
- {check_opencloud_security-1.2.2 → check_opencloud_security-1.2.3}/tests/test_explain.py +0 -0
- {check_opencloud_security-1.2.2 → check_opencloud_security-1.2.3}/tests/test_local_scanner.py +0 -0
- {check_opencloud_security-1.2.2 → check_opencloud_security-1.2.3}/tests/test_multi_host.py +0 -0
- {check_opencloud_security-1.2.2 → check_opencloud_security-1.2.3}/tests/test_perfdata.py +0 -0
- {check_opencloud_security-1.2.2 → check_opencloud_security-1.2.3}/tests/test_rating_thresholds.py +0 -0
- {check_opencloud_security-1.2.2 → check_opencloud_security-1.2.3}/tests/test_release_notes.py +0 -0
- {check_opencloud_security-1.2.2 → check_opencloud_security-1.2.3}/tests/test_release_schedule.py +0 -0
- {check_opencloud_security-1.2.2 → check_opencloud_security-1.2.3}/tests/test_releases.py +0 -0
- {check_opencloud_security-1.2.2 → check_opencloud_security-1.2.3}/tests/test_selfupdate.py +0 -0
- {check_opencloud_security-1.2.2 → check_opencloud_security-1.2.3}/tests/test_service.py +0 -0
- {check_opencloud_security-1.2.2 → check_opencloud_security-1.2.3}/tests/test_update_script.py +0 -0
- {check_opencloud_security-1.2.2 → check_opencloud_security-1.2.3}/tests/test_version.py +0 -0
- {check_opencloud_security-1.2.2 → check_opencloud_security-1.2.3}/tests/test_vulndb.py +0 -0
- {check_opencloud_security-1.2.2 → check_opencloud_security-1.2.3}/tests/test_waivers.py +0 -0
- {check_opencloud_security-1.2.2 → check_opencloud_security-1.2.3}/tests/test_wizard.py +0 -0
|
@@ -12,6 +12,14 @@ entry to `RELEASE.md` and uses it as the body of the GitHub release.
|
|
|
12
12
|
|
|
13
13
|
## [Unreleased]
|
|
14
14
|
|
|
15
|
+
## [1.2.3] - 2026-08-13
|
|
16
|
+
|
|
17
|
+
### Security
|
|
18
|
+
|
|
19
|
+
- Block webhook notifications to private, loopback, and link-local addresses
|
|
20
|
+
by default to prevent server-side request forgery. Internal receivers require
|
|
21
|
+
the explicit `--allow-private-webhooks` / `COS_ALLOW_PRIVATE_WEBHOOKS` opt-out.
|
|
22
|
+
|
|
15
23
|
## [1.2.2] - 2026-08-13
|
|
16
24
|
|
|
17
25
|
### Documentation
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: check-opencloud-security
|
|
3
|
-
Version: 1.2.
|
|
3
|
+
Version: 1.2.3
|
|
4
4
|
Summary: Check the security level of your OpenCloud instance with a built-in scanner - no API access required
|
|
5
5
|
Author: Massoud Ahmed
|
|
6
6
|
License: GNU GENERAL PUBLIC LICENSE
|
|
@@ -1194,6 +1194,7 @@ check-opencloud-security --host <Hostname> --check-hardening
|
|
|
1194
1194
|
| `--webhook-on` | Lowest state that triggers the webhook (`critical`, `warning`, `unknown`, `always`) | `critical` | `COS_WEBHOOK_ON` |
|
|
1195
1195
|
| `--webhook-header` | Extra header for the webhook request, repeatable | *None* | `COS_WEBHOOK_HEADERS` |
|
|
1196
1196
|
| `--webhook-timeout` | HTTP timeout in seconds for the webhook call | `10` | `COS_WEBHOOK_TIMEOUT` |
|
|
1197
|
+
| `--allow-private-webhooks` | Permit webhooks to private, loopback, or link-local addresses | *False* | `COS_ALLOW_PRIVATE_WEBHOOKS` |
|
|
1197
1198
|
| `--retries` | Retry attempts for transient network errors | `2` | `COS_RETRIES` |
|
|
1198
1199
|
| `--backoff-factor` | Exponential backoff factor (seconds) between retries | `0.5` | `COS_BACKOFF_FACTOR` |
|
|
1199
1200
|
| `--config` | Path to the configuration file (`.json` as JSON, else YAML) | auto-discovered | `COS_CONFIG_FILE` |
|
|
@@ -1979,6 +1980,10 @@ check-opencloud-security --host opencloud.example.com \
|
|
|
1979
1980
|
environment variable: `COS_WEBHOOK_HEADERS="X-Auth-Token: abc; X-Env: prod"`.
|
|
1980
1981
|
- `--webhook-timeout` / `COS_WEBHOOK_TIMEOUT` (default `10`) limits the
|
|
1981
1982
|
webhook call; it is independent of the scan `--timeout`.
|
|
1983
|
+
- Webhook destinations that resolve to private, loopback, or link-local
|
|
1984
|
+
addresses are blocked to prevent server-side request forgery. Set
|
|
1985
|
+
`--allow-private-webhooks` or `COS_ALLOW_PRIVATE_WEBHOOKS=true` only for an
|
|
1986
|
+
intentional internal receiver.
|
|
1982
1987
|
|
|
1983
1988
|
Delivery reuses `--retries` / `--backoff-factor`. **A failing webhook never
|
|
1984
1989
|
changes the check result** - the plugin appends `Webhook delivery failed` to
|
|
@@ -505,6 +505,7 @@ check-opencloud-security --host <Hostname> --check-hardening
|
|
|
505
505
|
| `--webhook-on` | Lowest state that triggers the webhook (`critical`, `warning`, `unknown`, `always`) | `critical` | `COS_WEBHOOK_ON` |
|
|
506
506
|
| `--webhook-header` | Extra header for the webhook request, repeatable | *None* | `COS_WEBHOOK_HEADERS` |
|
|
507
507
|
| `--webhook-timeout` | HTTP timeout in seconds for the webhook call | `10` | `COS_WEBHOOK_TIMEOUT` |
|
|
508
|
+
| `--allow-private-webhooks` | Permit webhooks to private, loopback, or link-local addresses | *False* | `COS_ALLOW_PRIVATE_WEBHOOKS` |
|
|
508
509
|
| `--retries` | Retry attempts for transient network errors | `2` | `COS_RETRIES` |
|
|
509
510
|
| `--backoff-factor` | Exponential backoff factor (seconds) between retries | `0.5` | `COS_BACKOFF_FACTOR` |
|
|
510
511
|
| `--config` | Path to the configuration file (`.json` as JSON, else YAML) | auto-discovered | `COS_CONFIG_FILE` |
|
|
@@ -1290,6 +1291,10 @@ check-opencloud-security --host opencloud.example.com \
|
|
|
1290
1291
|
environment variable: `COS_WEBHOOK_HEADERS="X-Auth-Token: abc; X-Env: prod"`.
|
|
1291
1292
|
- `--webhook-timeout` / `COS_WEBHOOK_TIMEOUT` (default `10`) limits the
|
|
1292
1293
|
webhook call; it is independent of the scan `--timeout`.
|
|
1294
|
+
- Webhook destinations that resolve to private, loopback, or link-local
|
|
1295
|
+
addresses are blocked to prevent server-side request forgery. Set
|
|
1296
|
+
`--allow-private-webhooks` or `COS_ALLOW_PRIVATE_WEBHOOKS=true` only for an
|
|
1297
|
+
intentional internal receiver.
|
|
1293
1298
|
|
|
1294
1299
|
Delivery reuses `--retries` / `--backoff-factor`. **A failing webhook never
|
|
1295
1300
|
changes the check result** - the plugin appends `Webhook delivery failed` to
|
{check_opencloud_security-1.2.2 → check_opencloud_security-1.2.3}/check_opencloud_security.py
RENAMED
|
@@ -16,6 +16,7 @@ import contextlib
|
|
|
16
16
|
import io
|
|
17
17
|
import ipaddress
|
|
18
18
|
import logging
|
|
19
|
+
import socket
|
|
19
20
|
import sys
|
|
20
21
|
import time
|
|
21
22
|
from collections import Counter
|
|
@@ -24,6 +25,7 @@ from dataclasses import dataclass
|
|
|
24
25
|
from datetime import datetime, timezone
|
|
25
26
|
from enum import IntEnum
|
|
26
27
|
from typing import Any, NoReturn, TypeVar
|
|
28
|
+
from urllib.parse import urlsplit
|
|
27
29
|
|
|
28
30
|
import requests
|
|
29
31
|
|
|
@@ -146,6 +148,7 @@ class ScanContext:
|
|
|
146
148
|
webhook_url: str | None = None
|
|
147
149
|
webhook_on: str = DEFAULT_WEBHOOK_ON
|
|
148
150
|
webhook_timeout: int = DEFAULT_WEBHOOK_TIMEOUT_SECONDS
|
|
151
|
+
allow_private_webhooks: bool = False
|
|
149
152
|
# Stored as a tuple of pairs so ScanContext stays hashable/frozen.
|
|
150
153
|
webhook_headers: tuple[tuple[str, str], ...] = ()
|
|
151
154
|
scanner_settings: ScannerSettings | None = None
|
|
@@ -196,6 +199,17 @@ def _env_bool(name: str) -> bool:
|
|
|
196
199
|
return _CONFIG.get_bool(name)
|
|
197
200
|
|
|
198
201
|
|
|
202
|
+
def _allow_private_webhooks_default() -> bool:
|
|
203
|
+
"""Read the webhook opt-out from its public environment or nested config key."""
|
|
204
|
+
name = "ALLOW_PRIVATE_WEBHOOKS"
|
|
205
|
+
if any(
|
|
206
|
+
f"{ENV_PREFIX}{suffix}" in _CONFIG.environ
|
|
207
|
+
for suffix in (name, f"{name}_FILE")
|
|
208
|
+
):
|
|
209
|
+
return _env_bool(name)
|
|
210
|
+
return _env_bool(f"WEBHOOK_{name}")
|
|
211
|
+
|
|
212
|
+
|
|
199
213
|
def _waiver_patterns(values: list[str] | None) -> tuple[str, ...] | None:
|
|
200
214
|
"""
|
|
201
215
|
Split repeated --ignore-hardening values into a flat list of patterns.
|
|
@@ -633,6 +647,12 @@ def _send_webhook(context: ScanContext, payload: dict[str, Any]) -> bool:
|
|
|
633
647
|
url = context.webhook_url
|
|
634
648
|
if not url:
|
|
635
649
|
return True
|
|
650
|
+
if not context.allow_private_webhooks and not _is_safe_webhook_url(url):
|
|
651
|
+
LOGGER.warning(
|
|
652
|
+
"Webhook URL points to a restricted private/local IP address and was blocked: %s",
|
|
653
|
+
url,
|
|
654
|
+
)
|
|
655
|
+
return False
|
|
636
656
|
|
|
637
657
|
headers = {"Content-Type": "application/json"}
|
|
638
658
|
headers.update(dict(context.webhook_headers))
|
|
@@ -665,6 +685,18 @@ def _send_webhook(context: ScanContext, payload: dict[str, Any]) -> bool:
|
|
|
665
685
|
return True
|
|
666
686
|
|
|
667
687
|
|
|
688
|
+
def _is_safe_webhook_url(url: str) -> bool:
|
|
689
|
+
"""Return whether url resolves to an address that is safe for a webhook."""
|
|
690
|
+
try:
|
|
691
|
+
hostname = urlsplit(url).hostname
|
|
692
|
+
if not hostname:
|
|
693
|
+
return False
|
|
694
|
+
address = ipaddress.ip_address(socket.gethostbyname(hostname))
|
|
695
|
+
except (ValueError, socket.gaierror):
|
|
696
|
+
return False
|
|
697
|
+
return not (address.is_private or address.is_loopback or address.is_link_local)
|
|
698
|
+
|
|
699
|
+
|
|
668
700
|
def _notify_and_fail(
|
|
669
701
|
context: ScanContext,
|
|
670
702
|
message: str,
|
|
@@ -1339,6 +1371,15 @@ def build_arg_parser() -> argparse.ArgumentParser:
|
|
|
1339
1371
|
f"Default: {DEFAULT_WEBHOOK_TIMEOUT_SECONDS} (env: {ENV_PREFIX}WEBHOOK_TIMEOUT)."
|
|
1340
1372
|
),
|
|
1341
1373
|
)
|
|
1374
|
+
parser.add_argument(
|
|
1375
|
+
"--allow-private-webhooks",
|
|
1376
|
+
action="store_true",
|
|
1377
|
+
default=_allow_private_webhooks_default(),
|
|
1378
|
+
help=(
|
|
1379
|
+
"Allow webhooks to private, loopback, and link-local addresses. "
|
|
1380
|
+
f"Default: False (env: {ENV_PREFIX}ALLOW_PRIVATE_WEBHOOKS)."
|
|
1381
|
+
),
|
|
1382
|
+
)
|
|
1342
1383
|
parser.add_argument(
|
|
1343
1384
|
"--backoff-factor",
|
|
1344
1385
|
type=float,
|
|
@@ -1495,6 +1536,7 @@ def _build_context(host: str, args: argparse.Namespace) -> ScanContext:
|
|
|
1495
1536
|
webhook_url=args.webhook_url,
|
|
1496
1537
|
webhook_on=args.webhook_on,
|
|
1497
1538
|
webhook_timeout=args.webhook_timeout,
|
|
1539
|
+
allow_private_webhooks=args.allow_private_webhooks,
|
|
1498
1540
|
webhook_headers=_parse_webhook_headers(args.webhook_header),
|
|
1499
1541
|
scanner_settings=scanner_settings,
|
|
1500
1542
|
release_settings=release_settings,
|
{check_opencloud_security-1.2.2 → check_opencloud_security-1.2.3}/opencloud_local_scan/wizard.py
RENAMED
|
@@ -334,6 +334,19 @@ def optional_groups() -> list[Group]:
|
|
|
334
334
|
example="X-Auth-Token: s3cr3t; X-Env: prod",
|
|
335
335
|
secret=True,
|
|
336
336
|
),
|
|
337
|
+
Question(
|
|
338
|
+
key="webhook.allow_private_webhooks",
|
|
339
|
+
prompt="Allow private webhook addresses",
|
|
340
|
+
explain=(
|
|
341
|
+
"Permit delivery to private, loopback, or link-local "
|
|
342
|
+
"addresses. Leave this off unless the receiver is an "
|
|
343
|
+
"intentional internal service."
|
|
344
|
+
),
|
|
345
|
+
example="no",
|
|
346
|
+
default="no",
|
|
347
|
+
validate=_choice(tuple(YES | NO)),
|
|
348
|
+
cast=lambda value: value.strip().lower() in YES,
|
|
349
|
+
),
|
|
337
350
|
],
|
|
338
351
|
),
|
|
339
352
|
Group(
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "check-opencloud-security"
|
|
3
|
-
version = "1.2.
|
|
3
|
+
version = "1.2.3"
|
|
4
4
|
description = "Check the security level of your OpenCloud instance with a built-in scanner - no API access required"
|
|
5
5
|
authors = [{ name = "Massoud Ahmed" }]
|
|
6
6
|
requires-python = ">=3.10"
|
|
@@ -67,6 +67,7 @@ def test_boolean_flags_are_read_from_the_environment(monkeypatch, reload_plugin)
|
|
|
67
67
|
monkeypatch.setenv("COS_INSECURE", "yes")
|
|
68
68
|
monkeypatch.setenv("COS_UPDATE_WARNING", "1")
|
|
69
69
|
monkeypatch.setenv("COS_NO_DEBUG_PORTS", "on")
|
|
70
|
+
monkeypatch.setenv("COS_ALLOW_PRIVATE_WEBHOOKS", "true")
|
|
70
71
|
|
|
71
72
|
module = reload_plugin()
|
|
72
73
|
args = module.build_arg_parser().parse_args([])
|
|
@@ -75,6 +76,7 @@ def test_boolean_flags_are_read_from_the_environment(monkeypatch, reload_plugin)
|
|
|
75
76
|
assert args.insecure is True
|
|
76
77
|
assert args.update_warning is True
|
|
77
78
|
assert args.no_debug_ports is True
|
|
79
|
+
assert args.allow_private_webhooks is True
|
|
78
80
|
|
|
79
81
|
|
|
80
82
|
@pytest.mark.parametrize("value", ["false", "no", "0", "off", ""])
|
|
@@ -161,6 +163,8 @@ def test_config_file_supplies_defaults(monkeypatch, reload_plugin, tmp_path):
|
|
|
161
163
|
"host: cloud.example.com\n"
|
|
162
164
|
"warning: 4\n"
|
|
163
165
|
"check_hardening: true\n"
|
|
166
|
+
"webhook:\n"
|
|
167
|
+
" allow_private_webhooks: true\n"
|
|
164
168
|
"scanner:\n"
|
|
165
169
|
" timeout: 25\n"
|
|
166
170
|
"releases:\n"
|
|
@@ -178,6 +182,7 @@ def test_config_file_supplies_defaults(monkeypatch, reload_plugin, tmp_path):
|
|
|
178
182
|
context = module._build_context("cloud.example.com", args)
|
|
179
183
|
assert context.scanner_settings.timeout == 25
|
|
180
184
|
assert context.release_settings.mode == "bundled"
|
|
185
|
+
assert context.allow_private_webhooks is True
|
|
181
186
|
|
|
182
187
|
|
|
183
188
|
def test_environment_beats_the_config_file(monkeypatch, reload_plugin, tmp_path):
|
|
@@ -46,7 +46,7 @@ def posts(monkeypatch):
|
|
|
46
46
|
|
|
47
47
|
def run(result, **kwargs):
|
|
48
48
|
"""Run the check and return its exit code."""
|
|
49
|
-
context = ScanContext(host="cloud.example.com", **kwargs)
|
|
49
|
+
context = ScanContext(host="cloud.example.com", allow_private_webhooks=True, **kwargs)
|
|
50
50
|
with pytest.raises(SystemExit) as excinfo:
|
|
51
51
|
plugin.check_vulnerabilities(
|
|
52
52
|
context, ScanResult(response=result, uuid="local-x"), duration_seconds=1.0
|
|
@@ -61,6 +61,11 @@ def test_no_webhook_url_means_no_request(posts):
|
|
|
61
61
|
assert posts == []
|
|
62
62
|
|
|
63
63
|
|
|
64
|
+
def test_private_webhook_protection_is_enabled_by_default():
|
|
65
|
+
"""Webhook destinations must be protected unless an operator opts out."""
|
|
66
|
+
assert ScanContext(host="cloud.example.com").allow_private_webhooks is False
|
|
67
|
+
|
|
68
|
+
|
|
64
69
|
def test_critical_result_fires_the_webhook(posts):
|
|
65
70
|
"""The default trigger is 'critical'."""
|
|
66
71
|
run(CRITICAL_RESULT, webhook_url="https://hooks.example.com/x")
|
|
@@ -172,6 +177,7 @@ def test_webhook_fires_for_an_unreachable_instance(posts, monkeypatch):
|
|
|
172
177
|
host="down.example.com",
|
|
173
178
|
webhook_url="https://x/",
|
|
174
179
|
webhook_on="unknown",
|
|
180
|
+
allow_private_webhooks=True,
|
|
175
181
|
scanner_settings=None,
|
|
176
182
|
)
|
|
177
183
|
|
|
@@ -196,3 +202,63 @@ def test_webhook_uses_the_configured_proxy(posts):
|
|
|
196
202
|
"http": "http://proxy:3128",
|
|
197
203
|
"https": "http://proxy:3128",
|
|
198
204
|
}
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def test_private_webhook_addresses_are_blocked(monkeypatch, caplog):
|
|
208
|
+
"""A webhook must not be usable to reach internal services by default."""
|
|
209
|
+
posted = []
|
|
210
|
+
monkeypatch.setattr(plugin.socket, "gethostbyname", lambda hostname: "127.0.0.1")
|
|
211
|
+
monkeypatch.setattr(
|
|
212
|
+
plugin.requests,
|
|
213
|
+
"post",
|
|
214
|
+
lambda *args, **kwargs: posted.append((args, kwargs)),
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
sent = plugin._send_webhook(
|
|
218
|
+
ScanContext(host="cloud.example.com", webhook_url="https://hooks.example.com/x"),
|
|
219
|
+
{"status": "CRITICAL"},
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
assert sent is False
|
|
223
|
+
assert posted == []
|
|
224
|
+
assert "restricted private/local IP address" in caplog.text
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def test_private_webhooks_require_an_explicit_opt_out(monkeypatch):
|
|
228
|
+
"""An intentional internal receiver remains available with an opt-out."""
|
|
229
|
+
posted = []
|
|
230
|
+
monkeypatch.setattr(plugin.socket, "gethostbyname", lambda hostname: "127.0.0.1")
|
|
231
|
+
|
|
232
|
+
class _Response:
|
|
233
|
+
def raise_for_status(self):
|
|
234
|
+
pass
|
|
235
|
+
|
|
236
|
+
monkeypatch.setattr(
|
|
237
|
+
plugin.requests,
|
|
238
|
+
"post",
|
|
239
|
+
lambda url, **kwargs: posted.append((url, kwargs)) or _Response(),
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
sent = plugin._send_webhook(
|
|
243
|
+
ScanContext(
|
|
244
|
+
host="cloud.example.com",
|
|
245
|
+
webhook_url="https://hooks.example.com/x",
|
|
246
|
+
allow_private_webhooks=True,
|
|
247
|
+
),
|
|
248
|
+
{"status": "CRITICAL"},
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
assert sent is True
|
|
252
|
+
assert posted[0][0] == "https://hooks.example.com/x"
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
@pytest.mark.parametrize("url", ["not a URL", "https://unresolvable.example.com/x"])
|
|
256
|
+
def test_invalid_or_unresolvable_webhook_urls_are_blocked(monkeypatch, url):
|
|
257
|
+
"""A malformed or unresolvable destination must fail closed."""
|
|
258
|
+
monkeypatch.setattr(
|
|
259
|
+
plugin.socket,
|
|
260
|
+
"gethostbyname",
|
|
261
|
+
lambda hostname: (_ for _ in ()).throw(plugin.socket.gaierror()),
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
assert plugin._is_safe_webhook_url(url) is False
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{check_opencloud_security-1.2.2 → check_opencloud_security-1.2.3}/opencloud_local_scan/README.md
RENAMED
|
File without changes
|
{check_opencloud_security-1.2.2 → check_opencloud_security-1.2.3}/opencloud_local_scan/__init__.py
RENAMED
|
File without changes
|
{check_opencloud_security-1.2.2 → check_opencloud_security-1.2.3}/opencloud_local_scan/baseline.py
RENAMED
|
File without changes
|
{check_opencloud_security-1.2.2 → check_opencloud_security-1.2.3}/opencloud_local_scan/cli.py
RENAMED
|
File without changes
|
{check_opencloud_security-1.2.2 → check_opencloud_security-1.2.3}/opencloud_local_scan/completion.py
RENAMED
|
File without changes
|
{check_opencloud_security-1.2.2 → check_opencloud_security-1.2.3}/opencloud_local_scan/config.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{check_opencloud_security-1.2.2 → check_opencloud_security-1.2.3}/opencloud_local_scan/factory.py
RENAMED
|
File without changes
|
{check_opencloud_security-1.2.2 → check_opencloud_security-1.2.3}/opencloud_local_scan/hardening.py
RENAMED
|
File without changes
|
{check_opencloud_security-1.2.2 → check_opencloud_security-1.2.3}/opencloud_local_scan/releases.py
RENAMED
|
File without changes
|
{check_opencloud_security-1.2.2 → check_opencloud_security-1.2.3}/opencloud_local_scan/scanner.py
RENAMED
|
File without changes
|
{check_opencloud_security-1.2.2 → check_opencloud_security-1.2.3}/opencloud_local_scan/secrets.py
RENAMED
|
File without changes
|
{check_opencloud_security-1.2.2 → check_opencloud_security-1.2.3}/opencloud_local_scan/selfupdate.py
RENAMED
|
File without changes
|
{check_opencloud_security-1.2.2 → check_opencloud_security-1.2.3}/opencloud_local_scan/service.py
RENAMED
|
File without changes
|
{check_opencloud_security-1.2.2 → check_opencloud_security-1.2.3}/opencloud_local_scan/versions.py
RENAMED
|
File without changes
|
{check_opencloud_security-1.2.2 → check_opencloud_security-1.2.3}/opencloud_local_scan/vulndb.py
RENAMED
|
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
|
{check_opencloud_security-1.2.2 → check_opencloud_security-1.2.3}/tests/test_local_scanner.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{check_opencloud_security-1.2.2 → check_opencloud_security-1.2.3}/tests/test_rating_thresholds.py
RENAMED
|
File without changes
|
{check_opencloud_security-1.2.2 → check_opencloud_security-1.2.3}/tests/test_release_notes.py
RENAMED
|
File without changes
|
{check_opencloud_security-1.2.2 → check_opencloud_security-1.2.3}/tests/test_release_schedule.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{check_opencloud_security-1.2.2 → check_opencloud_security-1.2.3}/tests/test_update_script.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|