test(backend): add pytest baseline (B0.8)
Unit (SQLite, pure logic):
- test_templating.py: Jinja2 sandbox, regex_extract, strict-undefined,
sandbox blocks attribute-access escape, output blob 10 MB cap.
- test_password.py: bcrypt hash + verify, empty / malformed handling.
- test_soc_token.py: 256-bit url-safe token + bcrypt verification.
- test_rbac_matrix.py: F11 invariants (lead ⊇ operator, SOC restricted
to detection + report-read, audit_read & ttp_promote lead-only).
- test_connector_factory.py: register / build / double-register-rejected,
TaskStatus terminal helper, Mythic mapping vs empty Home mapping.
- test_audit_hash.py: SHA-256 chain helper is deterministic and reacts
to prev_hash / metadata changes.
Integration scaffold (testcontainers Postgres):
- tests/integration/conftest.py spins up postgres:16-alpine, monkeypatches
MIMIC_DATABASE_URL, creates a Flask app + db.create_all.
- test_healthz.py: end-to-end smoke through the Flask test client.
38 unit tests pass; ruff clean.
2026-05-21 20:34:11 +02:00
|
|
|
"""Jinja2 sandbox + regex_extract tests."""
|
|
|
|
|
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
2026-05-21 20:44:48 +02:00
|
|
|
import gzip
|
|
|
|
|
|
test(backend): add pytest baseline (B0.8)
Unit (SQLite, pure logic):
- test_templating.py: Jinja2 sandbox, regex_extract, strict-undefined,
sandbox blocks attribute-access escape, output blob 10 MB cap.
- test_password.py: bcrypt hash + verify, empty / malformed handling.
- test_soc_token.py: 256-bit url-safe token + bcrypt verification.
- test_rbac_matrix.py: F11 invariants (lead ⊇ operator, SOC restricted
to detection + report-read, audit_read & ttp_promote lead-only).
- test_connector_factory.py: register / build / double-register-rejected,
TaskStatus terminal helper, Mythic mapping vs empty Home mapping.
- test_audit_hash.py: SHA-256 chain helper is deterministic and reacts
to prev_hash / metadata changes.
Integration scaffold (testcontainers Postgres):
- tests/integration/conftest.py spins up postgres:16-alpine, monkeypatches
MIMIC_DATABASE_URL, creates a Flask app + db.create_all.
- test_healthz.py: end-to-end smoke through the Flask test client.
38 unit tests pass; ruff clean.
2026-05-21 20:34:11 +02:00
|
|
|
import pytest
|
2026-05-21 20:44:48 +02:00
|
|
|
from jinja2 import TemplateError
|
test(backend): add pytest baseline (B0.8)
Unit (SQLite, pure logic):
- test_templating.py: Jinja2 sandbox, regex_extract, strict-undefined,
sandbox blocks attribute-access escape, output blob 10 MB cap.
- test_password.py: bcrypt hash + verify, empty / malformed handling.
- test_soc_token.py: 256-bit url-safe token + bcrypt verification.
- test_rbac_matrix.py: F11 invariants (lead ⊇ operator, SOC restricted
to detection + report-read, audit_read & ttp_promote lead-only).
- test_connector_factory.py: register / build / double-register-rejected,
TaskStatus terminal helper, Mythic mapping vs empty Home mapping.
- test_audit_hash.py: SHA-256 chain helper is deterministic and reacts
to prev_hash / metadata changes.
Integration scaffold (testcontainers Postgres):
- tests/integration/conftest.py spins up postgres:16-alpine, monkeypatches
MIMIC_DATABASE_URL, creates a Flask app + db.create_all.
- test_healthz.py: end-to-end smoke through the Flask test client.
38 unit tests pass; ruff clean.
2026-05-21 20:34:11 +02:00
|
|
|
|
|
|
|
|
from mimic.templating.filters import regex_extract
|
|
|
|
|
from mimic.templating.sandbox import (
|
|
|
|
|
CleanupRenderer,
|
|
|
|
|
RenderError,
|
|
|
|
|
StepOutputs,
|
|
|
|
|
render_cleanup,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestRegexExtract:
|
|
|
|
|
def test_returns_capture_group(self) -> None:
|
|
|
|
|
assert regex_extract("hello world", r"hello (\w+)") == "world"
|
|
|
|
|
|
2026-05-21 20:44:48 +02:00
|
|
|
def test_no_match_raises(self) -> None:
|
|
|
|
|
with pytest.raises(TemplateError, match="no match"):
|
|
|
|
|
regex_extract("hello", r"foo(\d+)")
|
|
|
|
|
|
|
|
|
|
def test_none_input_raises(self) -> None:
|
|
|
|
|
with pytest.raises(TemplateError, match="None"):
|
|
|
|
|
regex_extract(None, r"x")
|
|
|
|
|
|
|
|
|
|
def test_no_groups_falls_back_to_full_match(self) -> None:
|
|
|
|
|
assert regex_extract("abc123", r"\w+\d+") == "abc123"
|
test(backend): add pytest baseline (B0.8)
Unit (SQLite, pure logic):
- test_templating.py: Jinja2 sandbox, regex_extract, strict-undefined,
sandbox blocks attribute-access escape, output blob 10 MB cap.
- test_password.py: bcrypt hash + verify, empty / malformed handling.
- test_soc_token.py: 256-bit url-safe token + bcrypt verification.
- test_rbac_matrix.py: F11 invariants (lead ⊇ operator, SOC restricted
to detection + report-read, audit_read & ttp_promote lead-only).
- test_connector_factory.py: register / build / double-register-rejected,
TaskStatus terminal helper, Mythic mapping vs empty Home mapping.
- test_audit_hash.py: SHA-256 chain helper is deterministic and reacts
to prev_hash / metadata changes.
Integration scaffold (testcontainers Postgres):
- tests/integration/conftest.py spins up postgres:16-alpine, monkeypatches
MIMIC_DATABASE_URL, creates a Flask app + db.create_all.
- test_healthz.py: end-to-end smoke through the Flask test client.
38 unit tests pass; ruff clean.
2026-05-21 20:34:11 +02:00
|
|
|
|
2026-05-21 20:44:48 +02:00
|
|
|
def test_named_group(self) -> None:
|
|
|
|
|
assert regex_extract("pid=4242", r"pid=(?P<n>\d+)", name="n") == "4242"
|
test(backend): add pytest baseline (B0.8)
Unit (SQLite, pure logic):
- test_templating.py: Jinja2 sandbox, regex_extract, strict-undefined,
sandbox blocks attribute-access escape, output blob 10 MB cap.
- test_password.py: bcrypt hash + verify, empty / malformed handling.
- test_soc_token.py: 256-bit url-safe token + bcrypt verification.
- test_rbac_matrix.py: F11 invariants (lead ⊇ operator, SOC restricted
to detection + report-read, audit_read & ttp_promote lead-only).
- test_connector_factory.py: register / build / double-register-rejected,
TaskStatus terminal helper, Mythic mapping vs empty Home mapping.
- test_audit_hash.py: SHA-256 chain helper is deterministic and reacts
to prev_hash / metadata changes.
Integration scaffold (testcontainers Postgres):
- tests/integration/conftest.py spins up postgres:16-alpine, monkeypatches
MIMIC_DATABASE_URL, creates a Flask app + db.create_all.
- test_healthz.py: end-to-end smoke through the Flask test client.
38 unit tests pass; ruff clean.
2026-05-21 20:34:11 +02:00
|
|
|
|
2026-05-21 20:44:48 +02:00
|
|
|
def test_missing_named_group_raises(self) -> None:
|
|
|
|
|
with pytest.raises(TemplateError):
|
|
|
|
|
regex_extract("pid=4242", r"pid=(\d+)", name="absent")
|
test(backend): add pytest baseline (B0.8)
Unit (SQLite, pure logic):
- test_templating.py: Jinja2 sandbox, regex_extract, strict-undefined,
sandbox blocks attribute-access escape, output blob 10 MB cap.
- test_password.py: bcrypt hash + verify, empty / malformed handling.
- test_soc_token.py: 256-bit url-safe token + bcrypt verification.
- test_rbac_matrix.py: F11 invariants (lead ⊇ operator, SOC restricted
to detection + report-read, audit_read & ttp_promote lead-only).
- test_connector_factory.py: register / build / double-register-rejected,
TaskStatus terminal helper, Mythic mapping vs empty Home mapping.
- test_audit_hash.py: SHA-256 chain helper is deterministic and reacts
to prev_hash / metadata changes.
Integration scaffold (testcontainers Postgres):
- tests/integration/conftest.py spins up postgres:16-alpine, monkeypatches
MIMIC_DATABASE_URL, creates a Flask app + db.create_all.
- test_healthz.py: end-to-end smoke through the Flask test client.
38 unit tests pass; ruff clean.
2026-05-21 20:34:11 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestCleanupRenderer:
|
|
|
|
|
def setup_method(self) -> None:
|
|
|
|
|
self.renderer = CleanupRenderer()
|
|
|
|
|
|
|
|
|
|
def test_render_params(self) -> None:
|
|
|
|
|
out = self.renderer.render(
|
|
|
|
|
"echo {{ params.target }}",
|
|
|
|
|
params={"target": "WIN-01"},
|
|
|
|
|
)
|
|
|
|
|
assert out == "echo WIN-01"
|
|
|
|
|
|
|
|
|
|
def test_render_outputs_text(self) -> None:
|
|
|
|
|
out = self.renderer.render(
|
|
|
|
|
'echo "{{ outputs.text }}"',
|
|
|
|
|
outputs=StepOutputs(text="captured"),
|
|
|
|
|
)
|
|
|
|
|
assert out == 'echo "captured"'
|
|
|
|
|
|
|
|
|
|
def test_regex_extract_filter(self) -> None:
|
|
|
|
|
out = self.renderer.render(
|
|
|
|
|
r"{{ outputs.text | regex_extract('pid=(\\d+)') }}",
|
|
|
|
|
outputs=StepOutputs(text="status: pid=4242 user=svc"),
|
|
|
|
|
)
|
|
|
|
|
assert out == "4242"
|
|
|
|
|
|
2026-05-21 20:44:48 +02:00
|
|
|
def test_regex_extract_no_match_propagates_as_render_error(self) -> None:
|
|
|
|
|
with pytest.raises(RenderError, match="no match"):
|
|
|
|
|
self.renderer.render(
|
|
|
|
|
r"{{ outputs.text | regex_extract('pid=(\\d+)') }}",
|
|
|
|
|
outputs=StepOutputs(text="nothing"),
|
|
|
|
|
)
|
|
|
|
|
|
test(backend): add pytest baseline (B0.8)
Unit (SQLite, pure logic):
- test_templating.py: Jinja2 sandbox, regex_extract, strict-undefined,
sandbox blocks attribute-access escape, output blob 10 MB cap.
- test_password.py: bcrypt hash + verify, empty / malformed handling.
- test_soc_token.py: 256-bit url-safe token + bcrypt verification.
- test_rbac_matrix.py: F11 invariants (lead ⊇ operator, SOC restricted
to detection + report-read, audit_read & ttp_promote lead-only).
- test_connector_factory.py: register / build / double-register-rejected,
TaskStatus terminal helper, Mythic mapping vs empty Home mapping.
- test_audit_hash.py: SHA-256 chain helper is deterministic and reacts
to prev_hash / metadata changes.
Integration scaffold (testcontainers Postgres):
- tests/integration/conftest.py spins up postgres:16-alpine, monkeypatches
MIMIC_DATABASE_URL, creates a Flask app + db.create_all.
- test_healthz.py: end-to-end smoke through the Flask test client.
38 unit tests pass; ruff clean.
2026-05-21 20:34:11 +02:00
|
|
|
def test_strict_undefined_raises(self) -> None:
|
|
|
|
|
with pytest.raises(RenderError):
|
|
|
|
|
self.renderer.render("{{ params.does_not_exist }}", params={})
|
|
|
|
|
|
|
|
|
|
def test_sandbox_forbids_attribute_access(self) -> None:
|
|
|
|
|
with pytest.raises(RenderError):
|
|
|
|
|
self.renderer.render(
|
|
|
|
|
"{{ ().__class__.__bases__[0].__subclasses__() }}",
|
|
|
|
|
params={},
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def test_module_singleton_round_trip(self) -> None:
|
|
|
|
|
out = render_cleanup("hello {{ params.x }}", params={"x": "there"})
|
|
|
|
|
assert out == "hello there"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestStepOutputsBlob:
|
|
|
|
|
def test_blob_returns_empty_when_no_path(self) -> None:
|
|
|
|
|
out = StepOutputs(text="x")
|
|
|
|
|
assert out.blob() == ""
|
|
|
|
|
|
2026-05-21 20:44:48 +02:00
|
|
|
def test_blob_reads_gzipped_file(self, tmp_path) -> None:
|
|
|
|
|
blob = tmp_path / "blob.gz"
|
|
|
|
|
with gzip.open(blob, "wb") as fh:
|
|
|
|
|
fh.write(b"hello")
|
|
|
|
|
out = StepOutputs(blob_path=blob)
|
|
|
|
|
assert out.blob() == "hello"
|
|
|
|
|
|
|
|
|
|
def test_blob_caps_size_after_decompression(self, tmp_path) -> None:
|
|
|
|
|
blob = tmp_path / "blob.gz"
|
|
|
|
|
with gzip.open(blob, "wb") as fh:
|
|
|
|
|
fh.write(b"A" * 1024)
|
test(backend): add pytest baseline (B0.8)
Unit (SQLite, pure logic):
- test_templating.py: Jinja2 sandbox, regex_extract, strict-undefined,
sandbox blocks attribute-access escape, output blob 10 MB cap.
- test_password.py: bcrypt hash + verify, empty / malformed handling.
- test_soc_token.py: 256-bit url-safe token + bcrypt verification.
- test_rbac_matrix.py: F11 invariants (lead ⊇ operator, SOC restricted
to detection + report-read, audit_read & ttp_promote lead-only).
- test_connector_factory.py: register / build / double-register-rejected,
TaskStatus terminal helper, Mythic mapping vs empty Home mapping.
- test_audit_hash.py: SHA-256 chain helper is deterministic and reacts
to prev_hash / metadata changes.
Integration scaffold (testcontainers Postgres):
- tests/integration/conftest.py spins up postgres:16-alpine, monkeypatches
MIMIC_DATABASE_URL, creates a Flask app + db.create_all.
- test_healthz.py: end-to-end smoke through the Flask test client.
38 unit tests pass; ruff clean.
2026-05-21 20:34:11 +02:00
|
|
|
out = StepOutputs(blob_path=blob, blob_max_bytes=10)
|
|
|
|
|
assert out.blob() == "A" * 10
|
2026-05-21 20:44:48 +02:00
|
|
|
|
|
|
|
|
def test_blob_missing_file_returns_empty(self, tmp_path) -> None:
|
|
|
|
|
out = StepOutputs(blob_path=tmp_path / "absent.gz")
|
|
|
|
|
assert out.blob() == ""
|
|
|
|
|
|
|
|
|
|
def test_blob_non_gzip_returns_empty(self, tmp_path) -> None:
|
|
|
|
|
blob = tmp_path / "blob.gz"
|
|
|
|
|
blob.write_bytes(b"not actually gzip")
|
|
|
|
|
out = StepOutputs(blob_path=blob)
|
|
|
|
|
assert out.blob() == ""
|