Skip to content

Commit 4b1a8a1

Browse files
authored
Merge pull request #56 from mitre/fix/add-security-tests
Add security tests for emu plugin
2 parents 798d20c + 1db989a commit 4b1a8a1

File tree

1 file changed

+143
-0
lines changed

1 file changed

+143
-0
lines changed

tests/test_emu_security.py

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
import ast
2+
import os
3+
4+
import pytest
5+
import yaml
6+
7+
PLUGIN_DIR = os.path.join(os.path.dirname(__file__), '..')
8+
REQUIREMENTS_PATH = os.path.join(PLUGIN_DIR, 'requirements.txt')
9+
HOOK_PATH = os.path.join(PLUGIN_DIR, 'hook.py')
10+
CONF_DIR = os.path.join(PLUGIN_DIR, 'conf')
11+
DATA_DIR = os.path.join(PLUGIN_DIR, 'data')
12+
13+
14+
class TestRequirementsSecurity:
15+
"""Test that requirements.txt documents known CVE risks."""
16+
17+
def test_pyminizip_has_cve_warning_comment(self):
18+
"""pyminizip has known CVEs; requirements.txt should document this.
19+
20+
pyminizip uses zlib and has had vulnerabilities reported. A comment
21+
in requirements.txt should warn maintainers about this risk so it
22+
is not overlooked during dependency reviews.
23+
"""
24+
with open(REQUIREMENTS_PATH, 'r') as f:
25+
content = f.read()
26+
27+
# Verify pyminizip is listed
28+
assert 'pyminizip' in content, (
29+
"pyminizip not found in requirements.txt"
30+
)
31+
32+
# Check for a CVE-related comment near the pyminizip line
33+
lines = content.splitlines()
34+
found_cve_comment = False
35+
for i, line in enumerate(lines):
36+
if 'pyminizip' in line.lower():
37+
# Check this line and surrounding lines for CVE warning
38+
context_start = max(0, i - 2)
39+
context_end = min(len(lines), i + 3)
40+
context = '\n'.join(lines[context_start:context_end])
41+
if 'cve' in context.lower() or 'vulnerab' in context.lower():
42+
found_cve_comment = True
43+
break
44+
assert found_cve_comment, (
45+
"requirements.txt should have a comment warning about known "
46+
"CVEs for pyminizip (e.g., '# WARNING: pyminizip has known "
47+
"CVE vulnerabilities')"
48+
)
49+
50+
51+
class TestHookParseable:
52+
"""Verify that hook.py is syntactically valid."""
53+
54+
def test_hook_file_exists(self):
55+
"""hook.py must exist as the plugin entry point."""
56+
assert os.path.isfile(HOOK_PATH), (
57+
f"hook.py not found at {HOOK_PATH}"
58+
)
59+
60+
def test_hook_can_be_parsed(self):
61+
"""hook.py should be valid Python parseable by ast.parse."""
62+
with open(HOOK_PATH, 'r') as f:
63+
source = f.read()
64+
try:
65+
tree = ast.parse(source)
66+
except SyntaxError as e:
67+
pytest.fail(f"hook.py has a syntax error: {e}")
68+
assert tree is not None
69+
70+
def test_hook_defines_enable_function(self):
71+
"""hook.py must define an async 'enable' function."""
72+
with open(HOOK_PATH, 'r') as f:
73+
source = f.read()
74+
tree = ast.parse(source)
75+
enable_funcs = [
76+
node for node in ast.walk(tree)
77+
if isinstance(node, ast.AsyncFunctionDef) and node.name == 'enable'
78+
]
79+
assert len(enable_funcs) > 0, (
80+
"hook.py must define an 'async def enable(...)' function"
81+
)
82+
83+
def test_hook_defines_plugin_name(self):
84+
"""hook.py should define a 'name' variable."""
85+
with open(HOOK_PATH, 'r') as f:
86+
source = f.read()
87+
tree = ast.parse(source)
88+
name_assignments = [
89+
node for node in ast.walk(tree)
90+
if isinstance(node, ast.Assign)
91+
and any(
92+
isinstance(target, ast.Name) and target.id == 'name'
93+
for target in node.targets
94+
)
95+
]
96+
assert len(name_assignments) > 0, (
97+
"hook.py should define a 'name' variable for the plugin"
98+
)
99+
100+
101+
class TestAbilitiesYAML:
102+
"""Validate YAML configuration files are well-formed."""
103+
104+
def _get_yaml_files(self, directory):
105+
"""Recursively collect all YAML files from a directory."""
106+
yaml_files = []
107+
if not os.path.isdir(directory):
108+
return yaml_files
109+
for root, dirs, files in os.walk(directory):
110+
for fname in files:
111+
if fname.endswith('.yml') or fname.endswith('.yaml'):
112+
yaml_files.append(os.path.join(root, fname))
113+
return sorted(yaml_files)
114+
115+
def test_conf_yaml_files_are_valid(self):
116+
"""All YAML files in conf/ should be parseable."""
117+
yaml_files = self._get_yaml_files(CONF_DIR)
118+
assert len(yaml_files) > 0, (
119+
f"No YAML files found in {CONF_DIR}"
120+
)
121+
for fpath in yaml_files:
122+
with open(fpath, 'r') as f:
123+
try:
124+
data = yaml.safe_load(f)
125+
except yaml.YAMLError as e:
126+
pytest.fail(f"Failed to parse {fpath}: {e}")
127+
assert data is not None, (
128+
f"YAML file is empty: {fpath}"
129+
)
130+
131+
def test_data_yaml_files_are_valid_if_present(self):
132+
"""If data/ contains YAML files (post-setup), they should be parseable."""
133+
yaml_files = self._get_yaml_files(DATA_DIR)
134+
if not yaml_files:
135+
pytest.skip(
136+
"No YAML files in data/ — run plugin setup to populate"
137+
)
138+
for fpath in yaml_files:
139+
with open(fpath, 'r') as f:
140+
try:
141+
data = yaml.safe_load(f)
142+
except yaml.YAMLError as e:
143+
pytest.fail(f"Failed to parse {fpath}: {e}")

0 commit comments

Comments
 (0)