pysealer 0.2.0__cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl
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.
- pysealer/__init__.py +24 -0
- pysealer/_pysealer.cpython-314-arm-linux-gnueabihf.so +0 -0
- pysealer/add_decorators.py +215 -0
- pysealer/check_decorators.py +179 -0
- pysealer/cli.py +316 -0
- pysealer/dummy_decorators.py +83 -0
- pysealer/github_secrets.py +170 -0
- pysealer/remove_decorators.py +88 -0
- pysealer/setup.py +137 -0
- pysealer-0.2.0.dist-info/METADATA +171 -0
- pysealer-0.2.0.dist-info/RECORD +14 -0
- pysealer-0.2.0.dist-info/WHEEL +5 -0
- pysealer-0.2.0.dist-info/entry_points.txt +2 -0
- pysealer-0.2.0.dist-info/licenses/LICENSE +21 -0
pysealer/__init__.py
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""Pysealer package entry point
|
|
2
|
+
|
|
3
|
+
This package serves as a bridge between the Python command line interface and the
|
|
4
|
+
underlying Rust implementation (compiled as _pysealer module). It exposes the
|
|
5
|
+
core functionality for adding version control decorators to Python functions.
|
|
6
|
+
|
|
7
|
+
This module also dynamically provides decorator placeholders (e.g. @pysealer._<sig>)
|
|
8
|
+
so that decorated functions remain importable.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
# Define the rust to python module version and functions
|
|
12
|
+
from ._pysealer import generate_keypair, generate_signature, verify_signature
|
|
13
|
+
|
|
14
|
+
__version__ = "0.2.0"
|
|
15
|
+
__all__ = ["generate_keypair", "generate_signature", "verify_signature"]
|
|
16
|
+
|
|
17
|
+
# Ensure dummy decorators are registered on import
|
|
18
|
+
from . import dummy_decorators
|
|
19
|
+
|
|
20
|
+
# Allow dynamic decorator resolution for @pyseal._<sig>()
|
|
21
|
+
def __getattr__(name):
|
|
22
|
+
if name.startswith("_"):
|
|
23
|
+
return dummy_decorators._dummy_decorator
|
|
24
|
+
raise AttributeError(f"module 'pysealer' has no attribute '{name}'")
|
|
Binary file
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
"""Automatically add cryptographic decorators to all functions and classes in a python file."""
|
|
2
|
+
|
|
3
|
+
import ast
|
|
4
|
+
import copy
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from pysealer import generate_signature
|
|
7
|
+
from .setup import get_private_key
|
|
8
|
+
|
|
9
|
+
def add_decorators(file_path: str) -> str:
|
|
10
|
+
"""
|
|
11
|
+
Parse a Python file, add decorators to all functions and classes, and return the modified code.
|
|
12
|
+
|
|
13
|
+
Args:
|
|
14
|
+
file_path: Path to the Python file to process
|
|
15
|
+
|
|
16
|
+
Returns:
|
|
17
|
+
Modified Python source code as a string
|
|
18
|
+
"""
|
|
19
|
+
# Read the entire file content into a string
|
|
20
|
+
with open(file_path, 'r') as f:
|
|
21
|
+
content = f.read()
|
|
22
|
+
|
|
23
|
+
# Split content into lines for manipulation
|
|
24
|
+
lines = content.split('\n')
|
|
25
|
+
|
|
26
|
+
# Dynamically add 'import pysealer' grouped with other imports if not present
|
|
27
|
+
has_import_pysealer = any(
|
|
28
|
+
line.strip() == 'import pysealer' or line.strip().startswith('import pysealer') or line.strip().startswith('from pysealer')
|
|
29
|
+
for line in lines
|
|
30
|
+
)
|
|
31
|
+
if not has_import_pysealer:
|
|
32
|
+
# Find the import block
|
|
33
|
+
import_indices = [i for i, line in enumerate(lines) if line.strip().startswith('import ') or line.strip().startswith('from ')]
|
|
34
|
+
if import_indices:
|
|
35
|
+
# Insert after the last import in the block
|
|
36
|
+
last_import = import_indices[-1]
|
|
37
|
+
lines.insert(last_import + 1, 'import pysealer')
|
|
38
|
+
else:
|
|
39
|
+
# No import block found, insert after shebang/docstring/comments as before
|
|
40
|
+
insert_at = 0
|
|
41
|
+
if lines and lines[0].startswith('#!'):
|
|
42
|
+
insert_at = 1
|
|
43
|
+
while insert_at < len(lines) and (lines[insert_at].strip() == '' or lines[insert_at].strip().startswith('"""') or lines[insert_at].strip().startswith("''")):
|
|
44
|
+
if lines[insert_at].strip().startswith('"""') or lines[insert_at].strip().startswith("''"):
|
|
45
|
+
quote = lines[insert_at].strip()[:3]
|
|
46
|
+
insert_at += 1
|
|
47
|
+
while insert_at < len(lines) and not lines[insert_at].strip().endswith(quote):
|
|
48
|
+
insert_at += 1
|
|
49
|
+
if insert_at < len(lines):
|
|
50
|
+
insert_at += 1
|
|
51
|
+
else:
|
|
52
|
+
insert_at += 1
|
|
53
|
+
lines.insert(insert_at, 'import pysealer')
|
|
54
|
+
|
|
55
|
+
# Parse the Python source code into an Abstract Syntax Tree (AST)
|
|
56
|
+
content = '\n'.join(lines)
|
|
57
|
+
tree = ast.parse(content)
|
|
58
|
+
|
|
59
|
+
# First pass: Remove existing pysealer decorators
|
|
60
|
+
lines_to_remove = set()
|
|
61
|
+
for node in ast.walk(tree):
|
|
62
|
+
if type(node).__name__ in ("FunctionDef", "AsyncFunctionDef", "ClassDef"):
|
|
63
|
+
if hasattr(node, 'decorator_list'):
|
|
64
|
+
for decorator in node.decorator_list:
|
|
65
|
+
is_pysealer_decorator = False
|
|
66
|
+
|
|
67
|
+
if isinstance(decorator, ast.Name):
|
|
68
|
+
if decorator.id.startswith("pysealer"):
|
|
69
|
+
is_pysealer_decorator = True
|
|
70
|
+
elif isinstance(decorator, ast.Attribute):
|
|
71
|
+
if isinstance(decorator.value, ast.Name) and decorator.value.id == "pysealer":
|
|
72
|
+
is_pysealer_decorator = True
|
|
73
|
+
elif isinstance(decorator, ast.Call):
|
|
74
|
+
func = decorator.func
|
|
75
|
+
if isinstance(func, ast.Attribute):
|
|
76
|
+
if isinstance(func.value, ast.Name) and func.value.id == "pysealer":
|
|
77
|
+
is_pysealer_decorator = True
|
|
78
|
+
elif isinstance(func, ast.Name) and func.id.startswith("pysealer"):
|
|
79
|
+
is_pysealer_decorator = True
|
|
80
|
+
|
|
81
|
+
if is_pysealer_decorator:
|
|
82
|
+
# Mark this line for removal (convert to 0-indexed)
|
|
83
|
+
lines_to_remove.add(decorator.lineno - 1)
|
|
84
|
+
|
|
85
|
+
# Remove the marked lines (in reverse order to preserve indices)
|
|
86
|
+
for line_idx in sorted(lines_to_remove, reverse=True):
|
|
87
|
+
del lines[line_idx]
|
|
88
|
+
|
|
89
|
+
# Re-parse the content after removing decorators to get updated line numbers
|
|
90
|
+
modified_content = '\n'.join(lines)
|
|
91
|
+
tree = ast.parse(modified_content)
|
|
92
|
+
|
|
93
|
+
# Build parent map for all nodes
|
|
94
|
+
parent_map = {}
|
|
95
|
+
for parent in ast.walk(tree):
|
|
96
|
+
for child in ast.iter_child_nodes(parent):
|
|
97
|
+
parent_map[child] = parent
|
|
98
|
+
|
|
99
|
+
decorators_to_add = []
|
|
100
|
+
|
|
101
|
+
for node in ast.walk(tree):
|
|
102
|
+
node_type = type(node).__name__
|
|
103
|
+
|
|
104
|
+
# Only decorate:
|
|
105
|
+
# - Top-level functions (not inside a class)
|
|
106
|
+
# - Top-level classes
|
|
107
|
+
if node_type in ("FunctionDef", "AsyncFunctionDef"):
|
|
108
|
+
parent = parent_map.get(node)
|
|
109
|
+
if isinstance(parent, ast.ClassDef):
|
|
110
|
+
continue # skip methods inside classes
|
|
111
|
+
elif node_type == "ClassDef":
|
|
112
|
+
pass # always decorate classes
|
|
113
|
+
else:
|
|
114
|
+
continue
|
|
115
|
+
|
|
116
|
+
# Extract the complete source code of this function/class for hashing
|
|
117
|
+
node_clone = copy.deepcopy(node)
|
|
118
|
+
|
|
119
|
+
# Filter out pysealer decorators
|
|
120
|
+
if hasattr(node_clone, 'decorator_list'):
|
|
121
|
+
filtered_decorators = []
|
|
122
|
+
for decorator in node_clone.decorator_list:
|
|
123
|
+
should_keep = True
|
|
124
|
+
if isinstance(decorator, ast.Name):
|
|
125
|
+
if decorator.id.startswith("pysealer"):
|
|
126
|
+
should_keep = False
|
|
127
|
+
elif isinstance(decorator, ast.Attribute):
|
|
128
|
+
if isinstance(decorator.value, ast.Name) and decorator.value.id == "pysealer":
|
|
129
|
+
should_keep = False
|
|
130
|
+
elif isinstance(decorator, ast.Call):
|
|
131
|
+
func = decorator.func
|
|
132
|
+
if isinstance(func, ast.Attribute):
|
|
133
|
+
if isinstance(func.value, ast.Name) and func.value.id == "pysealer":
|
|
134
|
+
should_keep = False
|
|
135
|
+
elif isinstance(func, ast.Name) and func.id.startswith("pysealer"):
|
|
136
|
+
should_keep = False
|
|
137
|
+
if should_keep:
|
|
138
|
+
filtered_decorators.append(decorator)
|
|
139
|
+
node_clone.decorator_list = filtered_decorators
|
|
140
|
+
|
|
141
|
+
module_wrapper = ast.Module(body=[node_clone], type_ignores=[])
|
|
142
|
+
function_source = ast.unparse(module_wrapper)
|
|
143
|
+
|
|
144
|
+
try:
|
|
145
|
+
private_key = get_private_key()
|
|
146
|
+
except (FileNotFoundError, ValueError) as e:
|
|
147
|
+
raise RuntimeError(f"Cannot add decorators: {e}. Please run 'pysealer init' first.")
|
|
148
|
+
|
|
149
|
+
try:
|
|
150
|
+
signature = generate_signature(function_source, private_key)
|
|
151
|
+
except Exception as e:
|
|
152
|
+
raise RuntimeError(f"Failed to generate signature: {e}")
|
|
153
|
+
|
|
154
|
+
decorator_line = node.lineno - 1
|
|
155
|
+
if hasattr(node, 'decorator_list') and node.decorator_list:
|
|
156
|
+
decorator_line = node.decorator_list[0].lineno - 1
|
|
157
|
+
|
|
158
|
+
decorators_to_add.append((decorator_line, node.col_offset, signature))
|
|
159
|
+
|
|
160
|
+
# Sort in reverse order to add from bottom to top (preserves line numbers)
|
|
161
|
+
decorators_to_add.sort(reverse=True)
|
|
162
|
+
|
|
163
|
+
# Add decorators to the lines
|
|
164
|
+
for line_idx, col_offset, signature in decorators_to_add:
|
|
165
|
+
indent = ' ' * col_offset
|
|
166
|
+
decorator_line = f"{indent}@pysealer._{signature}()"
|
|
167
|
+
lines.insert(line_idx, decorator_line)
|
|
168
|
+
|
|
169
|
+
# Join lines back together
|
|
170
|
+
modified_code = '\n'.join(lines)
|
|
171
|
+
|
|
172
|
+
return modified_code
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def add_decorators_to_folder(folder_path: str) -> list[str]:
|
|
176
|
+
"""
|
|
177
|
+
Add decorators to all Python files in a folder.
|
|
178
|
+
|
|
179
|
+
Args:
|
|
180
|
+
folder_path: Path to the folder containing Python files
|
|
181
|
+
|
|
182
|
+
Returns:
|
|
183
|
+
List of file paths that were successfully decorated
|
|
184
|
+
"""
|
|
185
|
+
folder = Path(folder_path)
|
|
186
|
+
|
|
187
|
+
if not folder.exists():
|
|
188
|
+
raise FileNotFoundError(f"Folder '{folder_path}' does not exist.")
|
|
189
|
+
|
|
190
|
+
if not folder.is_dir():
|
|
191
|
+
raise NotADirectoryError(f"'{folder_path}' is not a directory.")
|
|
192
|
+
|
|
193
|
+
# Find all Python files in the folder (non-recursive)
|
|
194
|
+
python_files = list(folder.glob('*.py'))
|
|
195
|
+
|
|
196
|
+
if not python_files:
|
|
197
|
+
raise ValueError(f"No Python files found in '{folder_path}'.")
|
|
198
|
+
|
|
199
|
+
decorated_files = []
|
|
200
|
+
errors = []
|
|
201
|
+
|
|
202
|
+
for py_file in python_files:
|
|
203
|
+
try:
|
|
204
|
+
modified_code = add_decorators(str(py_file))
|
|
205
|
+
with open(py_file, 'w') as f:
|
|
206
|
+
f.write(modified_code)
|
|
207
|
+
decorated_files.append(str(py_file))
|
|
208
|
+
except Exception as e:
|
|
209
|
+
errors.append((str(py_file), str(e)))
|
|
210
|
+
|
|
211
|
+
if errors:
|
|
212
|
+
error_msg = "\n".join([f" - {file}: {error}" for file, error in errors])
|
|
213
|
+
raise RuntimeError(f"Failed to decorate some files:\n{error_msg}")
|
|
214
|
+
|
|
215
|
+
return decorated_files
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
"""Automatically verify cryptographic decorators for all functions and classes in a python file."""
|
|
2
|
+
|
|
3
|
+
import ast
|
|
4
|
+
import copy
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Dict
|
|
7
|
+
from pysealer import verify_signature
|
|
8
|
+
from .setup import get_public_key
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def check_decorators(file_path: str) -> Dict[str, dict]:
|
|
12
|
+
"""
|
|
13
|
+
Parse a Python file and verify all pysealer cryptographic decorators.
|
|
14
|
+
|
|
15
|
+
This function checks that each function/class with a pysealer decorator has a valid
|
|
16
|
+
signature that matches the current source code of that function/class.
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
file_path: Path to the Python file to verify
|
|
20
|
+
|
|
21
|
+
Returns:
|
|
22
|
+
Dictionary mapping function/class names to their verification results:
|
|
23
|
+
{
|
|
24
|
+
"function_name": {
|
|
25
|
+
"valid": bool, # Whether signature is valid
|
|
26
|
+
"signature": str, # The signature found in decorator
|
|
27
|
+
"message": str, # Success or error message
|
|
28
|
+
"has_decorator": bool # Whether function has pysealer decorator
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
"""
|
|
32
|
+
# Read the file content
|
|
33
|
+
with open(file_path, 'r') as f:
|
|
34
|
+
content = f.read()
|
|
35
|
+
|
|
36
|
+
# Parse the Python source code into an AST
|
|
37
|
+
tree = ast.parse(content)
|
|
38
|
+
|
|
39
|
+
# Get the public key for verification
|
|
40
|
+
try:
|
|
41
|
+
public_key = get_public_key()
|
|
42
|
+
except (FileNotFoundError, ValueError) as e:
|
|
43
|
+
raise RuntimeError(f"Cannot verify decorators: {e}. Please run 'pysealer init' first.")
|
|
44
|
+
|
|
45
|
+
# Dictionary to store results
|
|
46
|
+
results = {}
|
|
47
|
+
|
|
48
|
+
# Iterate through each node in the AST
|
|
49
|
+
for node in ast.walk(tree):
|
|
50
|
+
node_type = type(node).__name__
|
|
51
|
+
|
|
52
|
+
# Check if this node is a function or class definition
|
|
53
|
+
if node_type in ("FunctionDef", "AsyncFunctionDef", "ClassDef"):
|
|
54
|
+
name = node.name
|
|
55
|
+
|
|
56
|
+
# Look for pysealer decorator
|
|
57
|
+
signature_from_decorator = None
|
|
58
|
+
has_pysealer_decorator = False
|
|
59
|
+
|
|
60
|
+
if hasattr(node, 'decorator_list'):
|
|
61
|
+
for decorator in node.decorator_list:
|
|
62
|
+
# Check if decorator is a Call node (e.g., @pysealer._<signature>())
|
|
63
|
+
if isinstance(decorator, ast.Call):
|
|
64
|
+
func = decorator.func
|
|
65
|
+
# Check if it's pysealer._<signature>
|
|
66
|
+
if isinstance(func, ast.Attribute):
|
|
67
|
+
if isinstance(func.value, ast.Name) and func.value.id == "pysealer":
|
|
68
|
+
attr_name = func.attr
|
|
69
|
+
# Extract signature (remove leading underscore)
|
|
70
|
+
if attr_name.startswith('_'):
|
|
71
|
+
signature_from_decorator = attr_name[1:]
|
|
72
|
+
has_pysealer_decorator = True
|
|
73
|
+
break
|
|
74
|
+
|
|
75
|
+
# Initialize result for this function/class
|
|
76
|
+
result = {
|
|
77
|
+
"has_decorator": has_pysealer_decorator,
|
|
78
|
+
"valid": False,
|
|
79
|
+
"signature": signature_from_decorator,
|
|
80
|
+
"message": ""
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if not has_pysealer_decorator:
|
|
84
|
+
result["message"] = "No pysealer decorator found"
|
|
85
|
+
results[name] = result
|
|
86
|
+
continue
|
|
87
|
+
|
|
88
|
+
# Extract the source code without pysealer decorators for verification
|
|
89
|
+
node_clone = copy.deepcopy(node)
|
|
90
|
+
|
|
91
|
+
# Filter out pysealer decorators from the clone
|
|
92
|
+
if hasattr(node_clone, 'decorator_list'):
|
|
93
|
+
filtered_decorators = []
|
|
94
|
+
|
|
95
|
+
for decorator in node_clone.decorator_list:
|
|
96
|
+
should_keep = True
|
|
97
|
+
|
|
98
|
+
# Check if decorator is a simple Name node starting with "pysealer"
|
|
99
|
+
if isinstance(decorator, ast.Name):
|
|
100
|
+
if decorator.id.startswith("pysealer"):
|
|
101
|
+
should_keep = False
|
|
102
|
+
|
|
103
|
+
# Check if decorator is an Attribute node (e.g., pysealer.something)
|
|
104
|
+
elif isinstance(decorator, ast.Attribute):
|
|
105
|
+
if isinstance(decorator.value, ast.Name) and decorator.value.id == "pysealer":
|
|
106
|
+
should_keep = False
|
|
107
|
+
|
|
108
|
+
# Check if decorator is a Call node
|
|
109
|
+
elif isinstance(decorator, ast.Call):
|
|
110
|
+
func = decorator.func
|
|
111
|
+
# Check if call is to pysealer.something()
|
|
112
|
+
if isinstance(func, ast.Attribute):
|
|
113
|
+
if isinstance(func.value, ast.Name) and func.value.id == "pysealer":
|
|
114
|
+
should_keep = False
|
|
115
|
+
# Check if call is to pysealer_something()
|
|
116
|
+
elif isinstance(func, ast.Name) and func.id.startswith("pysealer"):
|
|
117
|
+
should_keep = False
|
|
118
|
+
|
|
119
|
+
if should_keep:
|
|
120
|
+
filtered_decorators.append(decorator)
|
|
121
|
+
|
|
122
|
+
node_clone.decorator_list = filtered_decorators
|
|
123
|
+
|
|
124
|
+
# Convert the filtered node back to source code
|
|
125
|
+
module_wrapper = ast.Module(body=[node_clone], type_ignores=[])
|
|
126
|
+
function_source = ast.unparse(module_wrapper)
|
|
127
|
+
|
|
128
|
+
# Verify the signature
|
|
129
|
+
try:
|
|
130
|
+
is_valid = verify_signature(function_source, signature_from_decorator, public_key)
|
|
131
|
+
|
|
132
|
+
result["valid"] = is_valid
|
|
133
|
+
if is_valid:
|
|
134
|
+
result["message"] = "✓ Signature valid - code has not been tampered with"
|
|
135
|
+
else:
|
|
136
|
+
result["message"] = "✗ Signature invalid - code may have been modified"
|
|
137
|
+
|
|
138
|
+
except Exception as e:
|
|
139
|
+
result["message"] = f"✗ Error verifying signature: {e}"
|
|
140
|
+
|
|
141
|
+
results[name] = result
|
|
142
|
+
|
|
143
|
+
return results
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def check_decorators_in_folder(folder_path: str) -> Dict[str, Dict[str, dict]]:
|
|
147
|
+
"""
|
|
148
|
+
Check decorators in all Python files in a folder.
|
|
149
|
+
|
|
150
|
+
Args:
|
|
151
|
+
folder_path: Path to the folder containing Python files
|
|
152
|
+
|
|
153
|
+
Returns:
|
|
154
|
+
Dictionary mapping file paths to their verification results
|
|
155
|
+
"""
|
|
156
|
+
folder = Path(folder_path)
|
|
157
|
+
|
|
158
|
+
if not folder.exists():
|
|
159
|
+
raise FileNotFoundError(f"Folder '{folder_path}' does not exist.")
|
|
160
|
+
|
|
161
|
+
if not folder.is_dir():
|
|
162
|
+
raise NotADirectoryError(f"'{folder_path}' is not a directory.")
|
|
163
|
+
|
|
164
|
+
# Find all Python files in the folder (non-recursive)
|
|
165
|
+
python_files = list(folder.glob('*.py'))
|
|
166
|
+
|
|
167
|
+
if not python_files:
|
|
168
|
+
raise ValueError(f"No Python files found in '{folder_path}'.")
|
|
169
|
+
|
|
170
|
+
all_results = {}
|
|
171
|
+
|
|
172
|
+
for py_file in python_files:
|
|
173
|
+
try:
|
|
174
|
+
results = check_decorators(str(py_file))
|
|
175
|
+
all_results[str(py_file)] = results
|
|
176
|
+
except Exception as e:
|
|
177
|
+
all_results[str(py_file)] = {"error": str(e)}
|
|
178
|
+
|
|
179
|
+
return all_results
|