fix(security): close public metrics exposure
This commit is contained in:
85
tests/test_metrics_edge_security.py
Normal file
85
tests/test_metrics_edge_security.py
Normal file
@@ -0,0 +1,85 @@
|
||||
from pathlib import Path
|
||||
|
||||
from flask import Blueprint, Flask, jsonify
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
def _metrics_app() -> Flask:
|
||||
from services.http_access_policy_service import enforce_http_access_policy
|
||||
|
||||
app = Flask(__name__)
|
||||
system_public = Blueprint("system_public", __name__)
|
||||
|
||||
@system_public.route("/metrics")
|
||||
def prometheus_metrics():
|
||||
return jsonify({"status": "up"})
|
||||
|
||||
app.register_blueprint(system_public)
|
||||
app.before_request(enforce_http_access_policy)
|
||||
return app
|
||||
|
||||
|
||||
def test_metrics_network_classifier_is_deny_by_default():
|
||||
from services.http_access_policy_service import is_internal_metrics_request
|
||||
|
||||
assert is_internal_metrics_request("127.0.0.1") is True
|
||||
assert is_internal_metrics_request("172.20.0.4") is True
|
||||
assert is_internal_metrics_request("127.0.0.1", "172.20.0.4") is True
|
||||
assert is_internal_metrics_request("127.0.0.1", "203.0.113.10") is False
|
||||
assert is_internal_metrics_request("127.0.0.1", "192.168.0.110") is False
|
||||
assert is_internal_metrics_request("203.0.113.10", "172.20.0.4") is False
|
||||
assert is_internal_metrics_request("127.0.0.1", "not-an-ip") is False
|
||||
|
||||
|
||||
def test_metrics_route_denies_public_and_lan_but_allows_internal_transport():
|
||||
client = _metrics_app().test_client()
|
||||
|
||||
public = client.get(
|
||||
"/metrics",
|
||||
environ_base={"REMOTE_ADDR": "127.0.0.1"},
|
||||
headers={"X-Real-IP": "203.0.113.10"},
|
||||
)
|
||||
lan = client.get(
|
||||
"/metrics",
|
||||
environ_base={"REMOTE_ADDR": "127.0.0.1"},
|
||||
headers={"X-Real-IP": "192.168.0.110"},
|
||||
)
|
||||
internal = client.get(
|
||||
"/metrics",
|
||||
environ_base={"REMOTE_ADDR": "127.0.0.1"},
|
||||
headers={"X-Real-IP": "172.20.0.4"},
|
||||
)
|
||||
direct = client.get("/metrics", environ_base={"REMOTE_ADDR": "172.18.0.5"})
|
||||
|
||||
assert public.status_code == 404
|
||||
assert public.get_json() == {"error": "not_found"}
|
||||
assert lan.status_code == 404
|
||||
assert internal.status_code == 200
|
||||
assert direct.status_code == 200
|
||||
|
||||
|
||||
def test_source_owned_edge_and_prometheus_templates_are_closed_by_default():
|
||||
nginx = (ROOT / "monitoring/edge/ewoooc_metrics_nginx_location.conf").read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
prometheus = (ROOT / "monitoring/edge/ewoooc_prometheus_scrape.yml").read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
|
||||
assert "location = /metrics" in nginx
|
||||
assert "allow 172.16.0.0/12;" in nginx
|
||||
assert "deny all;" in nginx
|
||||
assert "proxy_pass http://127.0.0.1:5003/metrics;" in nginx
|
||||
assert "job_name: 'ewoooc-app'" in prometheus
|
||||
assert "metrics_path: /metrics" in prometheus
|
||||
assert "targets: ['mo.wooo.work']" in prometheus
|
||||
|
||||
|
||||
def test_monitoring_compose_has_no_plaintext_admin_password_or_public_prometheus_port():
|
||||
compose = (ROOT / "monitoring/docker-compose.yml").read_text(encoding="utf-8")
|
||||
|
||||
assert "GF_SECURITY_ADMIN_PASSWORD=${GRAFANA_ADMIN_PASSWORD:?" in compose
|
||||
assert "127.0.0.1:9090:9090" in compose
|
||||
assert "\n - \"9090:9090\"" not in compose
|
||||
@@ -32,7 +32,7 @@ def test_security_governance_review_is_honest_and_release_gate_passes():
|
||||
access = checks["PR.AA-route-access-control"]["evidence"]
|
||||
assert access["mutating_unguarded_count"] == 0
|
||||
assert access["unclassified_unguarded_count"] == 0
|
||||
assert access["unguarded_count"] == 8
|
||||
assert access["unguarded_count"] == 7
|
||||
dependencies = checks["ID.RA-dependency-vulnerability-management"]["evidence"]
|
||||
assert dependencies["sbom_files"] == []
|
||||
supply = checks["GV.SC-gitea-only-supply-chain"]
|
||||
@@ -149,7 +149,9 @@ def test_runtime_receipt_advances_access_readback_without_hiding_remaining_debt(
|
||||
"secure_cookie": True,
|
||||
"sensitive_route_count": 8,
|
||||
"denied_route_count": 8,
|
||||
"metrics_public": True,
|
||||
"metrics_public": False,
|
||||
"metrics_internal_canary": True,
|
||||
"metrics_target_health": "up",
|
||||
},
|
||||
"database": {"unchanged": True},
|
||||
"post_verifier": {
|
||||
@@ -162,7 +164,7 @@ def test_runtime_receipt_advances_access_readback_without_hiding_remaining_debt(
|
||||
"blocker": "no_ewoooc_host_runner",
|
||||
},
|
||||
"learning_recorded": False,
|
||||
"next_machine_action": "move_metrics_behind_edge_auth_with_prometheus_canary",
|
||||
"next_machine_action": "inventory_active_users_then_shadow_database_auth_before_cutover",
|
||||
}
|
||||
(governance / "security_governance_runtime_receipt.json").write_text(
|
||||
json.dumps(payload, ensure_ascii=False),
|
||||
@@ -173,11 +175,42 @@ def test_runtime_receipt_advances_access_readback_without_hiding_remaining_debt(
|
||||
|
||||
assert receipt["security_readback_passed"] is True
|
||||
assert receipt["database_unchanged"] is True
|
||||
assert receipt["metrics_public"] is True
|
||||
assert receipt["metrics_public"] is False
|
||||
assert receipt["metrics_internal_canary"] is True
|
||||
assert receipt["metrics_target_health"] == "up"
|
||||
assert receipt["cd_status"] == "waiting"
|
||||
assert receipt["learning_recorded"] is False
|
||||
|
||||
|
||||
def test_runtime_receipt_rejects_public_metrics_even_when_other_checks_pass(tmp_path):
|
||||
from services.security_governance_review_service import _runtime_receipt
|
||||
|
||||
governance = tmp_path / "governance"
|
||||
governance.mkdir()
|
||||
payload = {
|
||||
"receipt_kind": "security_governance_runtime_receipt",
|
||||
"source_commit": "d" * 40,
|
||||
"access_control": {
|
||||
"status": "pass",
|
||||
"sensitive_routes_denied": True,
|
||||
"security_headers_passed": True,
|
||||
"secure_cookie": True,
|
||||
"metrics_public": True,
|
||||
"metrics_internal_canary": True,
|
||||
},
|
||||
"database": {"unchanged": True},
|
||||
"post_verifier": {"health_status": "healthy", "governance_gate": "pass"},
|
||||
}
|
||||
(governance / "security_governance_runtime_receipt.json").write_text(
|
||||
json.dumps(payload),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
receipt = _runtime_receipt(tmp_path)
|
||||
|
||||
assert receipt["security_readback_passed"] is False
|
||||
|
||||
|
||||
def test_sensitive_blueprint_policy_denies_anonymous_api(monkeypatch):
|
||||
import auth
|
||||
from services.http_access_policy_service import enforce_http_access_policy
|
||||
|
||||
Reference in New Issue
Block a user