108 lines
3.3 KiB
Python
108 lines
3.3 KiB
Python
"""Tests for the generation-time render-safety guardrails.
|
|
|
|
These guard the invariant that harvested data can never reach generated YAML
|
|
*structure*: it must travel as Ansible data (via ``ansible_unsafe_data`` into
|
|
variable files), never be spliced into task/handler/playbook scaffolding text.
|
|
"""
|
|
|
|
import pytest
|
|
|
|
from enroll.render_safety import (
|
|
AnsibleUnsafeText,
|
|
RenderSafetyError,
|
|
ansible_unsafe_data,
|
|
assert_generated_yaml_safe,
|
|
is_ansible_template_like,
|
|
scaffold_token,
|
|
)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"value",
|
|
[
|
|
"net",
|
|
"admin",
|
|
"section_misc_2",
|
|
"docker_service",
|
|
"enroll_restart_grouped_services_net",
|
|
"host.example.com", # fqdn-like, dots/hyphens allowed
|
|
"a-b_c.d",
|
|
],
|
|
)
|
|
def test_scaffold_token_accepts_safe_identifiers(value):
|
|
assert scaffold_token(value) == value
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"value",
|
|
[
|
|
"evil.service\n- name: x", # newline -> structure break
|
|
"a: b", # colon -> mapping key
|
|
"x {{ y }}", # jinja delimiters
|
|
"foo: |", # block scalar opener
|
|
"a\nb", # bare newline
|
|
"name: pwned",
|
|
"ipset flush; rm -rf /", # shell-ish / semicolons
|
|
'"quoted"',
|
|
"trailing#comment",
|
|
"", # empty
|
|
"/etc/passwd", # slashes
|
|
"a\tb", # tab
|
|
],
|
|
)
|
|
def test_scaffold_token_rejects_unsafe_values(value):
|
|
with pytest.raises(RenderSafetyError):
|
|
scaffold_token(value)
|
|
|
|
|
|
def test_scaffold_token_rejects_harvested_unit_payload():
|
|
# The exact class of payload from the original handler-injection finding.
|
|
payload = (
|
|
"evil.service\n"
|
|
" ansible.builtin.command: touch /tmp/pwned\n"
|
|
"- name: INJECTED\n"
|
|
)
|
|
with pytest.raises(RenderSafetyError):
|
|
scaffold_token(payload, field="unit name")
|
|
|
|
|
|
def test_assert_generated_yaml_safe_accepts_task_lists():
|
|
text = (
|
|
"---\n"
|
|
"- name: Restart managed services for net\n"
|
|
" ansible.builtin.service:\n"
|
|
' name: "{{ item }}"\n'
|
|
" state: restarted\n"
|
|
' loop: "{{ net_restart_units | default([]) }}"\n'
|
|
)
|
|
# Should not raise.
|
|
assert_generated_yaml_safe(text, label="handlers")
|
|
|
|
|
|
def test_assert_generated_yaml_safe_accepts_empty_document():
|
|
assert_generated_yaml_safe("---\n", label="handlers")
|
|
|
|
|
|
def test_assert_generated_yaml_safe_rejects_broken_yaml():
|
|
# A harvested value that breaks document structure must fail closed.
|
|
broken = "---\n- name: a\n x: : :\n"
|
|
with pytest.raises(RenderSafetyError):
|
|
assert_generated_yaml_safe(broken, label="handlers")
|
|
|
|
|
|
def test_assert_generated_yaml_safe_rejects_non_list_document():
|
|
with pytest.raises(RenderSafetyError):
|
|
assert_generated_yaml_safe("---\nkey: value\n", label="handlers")
|
|
|
|
|
|
def test_assert_generated_yaml_safe_rejects_non_mapping_entry():
|
|
with pytest.raises(RenderSafetyError):
|
|
assert_generated_yaml_safe("---\n- just_a_string\n", label="handlers")
|
|
|
|
|
|
def test_ansible_unsafe_data_still_tags_jinja_values():
|
|
out = ansible_unsafe_data({"k": "{{ danger }}", "safe": "plain"})
|
|
assert isinstance(out["k"], AnsibleUnsafeText)
|
|
assert not isinstance(out["safe"], AnsibleUnsafeText)
|
|
assert is_ansible_template_like("{{ x }}")
|
|
assert not is_ansible_template_like("plain")
|