pysealer 0.6.0__pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.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.pypy311-pp73-aarch64-linux-gnu.so +0 -0
- pysealer/add_decorators.py +228 -0
- pysealer/check_decorators.py +186 -0
- pysealer/cli.py +367 -0
- pysealer/dummy_decorators.py +83 -0
- pysealer/git_diff.py +228 -0
- pysealer/github_secrets.py +175 -0
- pysealer/remove_decorators.py +88 -0
- pysealer/setup.py +137 -0
- pysealer-0.6.0.dist-info/METADATA +171 -0
- pysealer-0.6.0.dist-info/RECORD +15 -0
- pysealer-0.6.0.dist-info/WHEEL +5 -0
- pysealer-0.6.0.dist-info/entry_points.txt +2 -0
- pysealer-0.6.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.6.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,228 @@
|
|
|
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) -> tuple[str, bool]:
|
|
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
|
+
Tuple of (modified Python source code as a string, whether any decorators were added)
|
|
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
|
+
# Parse the Python source code into an Abstract Syntax Tree (AST)
|
|
27
|
+
tree = ast.parse(content)
|
|
28
|
+
|
|
29
|
+
# First pass: Remove existing pysealer decorators
|
|
30
|
+
lines_to_remove = set()
|
|
31
|
+
for node in ast.walk(tree):
|
|
32
|
+
if type(node).__name__ in ("FunctionDef", "AsyncFunctionDef", "ClassDef"):
|
|
33
|
+
if hasattr(node, 'decorator_list'):
|
|
34
|
+
for decorator in node.decorator_list:
|
|
35
|
+
is_pysealer_decorator = False
|
|
36
|
+
|
|
37
|
+
if isinstance(decorator, ast.Name):
|
|
38
|
+
if decorator.id.startswith("pysealer"):
|
|
39
|
+
is_pysealer_decorator = True
|
|
40
|
+
elif isinstance(decorator, ast.Attribute):
|
|
41
|
+
if isinstance(decorator.value, ast.Name) and decorator.value.id == "pysealer":
|
|
42
|
+
is_pysealer_decorator = True
|
|
43
|
+
elif isinstance(decorator, ast.Call):
|
|
44
|
+
func = decorator.func
|
|
45
|
+
if isinstance(func, ast.Attribute):
|
|
46
|
+
if isinstance(func.value, ast.Name) and func.value.id == "pysealer":
|
|
47
|
+
is_pysealer_decorator = True
|
|
48
|
+
elif isinstance(func, ast.Name) and func.id.startswith("pysealer"):
|
|
49
|
+
is_pysealer_decorator = True
|
|
50
|
+
|
|
51
|
+
if is_pysealer_decorator:
|
|
52
|
+
# Mark this line for removal (convert to 0-indexed)
|
|
53
|
+
lines_to_remove.add(decorator.lineno - 1)
|
|
54
|
+
|
|
55
|
+
# Remove the marked lines (in reverse order to preserve indices)
|
|
56
|
+
for line_idx in sorted(lines_to_remove, reverse=True):
|
|
57
|
+
del lines[line_idx]
|
|
58
|
+
|
|
59
|
+
# Re-parse the content after removing decorators to get updated line numbers
|
|
60
|
+
modified_content = '\n'.join(lines)
|
|
61
|
+
tree = ast.parse(modified_content)
|
|
62
|
+
|
|
63
|
+
# Build parent map for all nodes
|
|
64
|
+
parent_map = {}
|
|
65
|
+
for parent in ast.walk(tree):
|
|
66
|
+
for child in ast.iter_child_nodes(parent):
|
|
67
|
+
parent_map[child] = parent
|
|
68
|
+
|
|
69
|
+
decorators_to_add = []
|
|
70
|
+
|
|
71
|
+
for node in ast.walk(tree):
|
|
72
|
+
node_type = type(node).__name__
|
|
73
|
+
|
|
74
|
+
# Only decorate:
|
|
75
|
+
# - Top-level functions (not inside a class)
|
|
76
|
+
# - Top-level classes
|
|
77
|
+
if node_type in ("FunctionDef", "AsyncFunctionDef"):
|
|
78
|
+
parent = parent_map.get(node)
|
|
79
|
+
if isinstance(parent, ast.ClassDef):
|
|
80
|
+
continue # skip methods inside classes
|
|
81
|
+
elif node_type == "ClassDef":
|
|
82
|
+
pass # always decorate classes
|
|
83
|
+
else:
|
|
84
|
+
continue
|
|
85
|
+
|
|
86
|
+
# Extract the complete source code of this function/class for hashing
|
|
87
|
+
# Use original source to preserve formatting (quotes, spacing, etc.)
|
|
88
|
+
start_line = node.lineno - 1
|
|
89
|
+
end_line = node.end_lineno if hasattr(node, 'end_lineno') and node.end_lineno else node.lineno
|
|
90
|
+
|
|
91
|
+
# Get the source lines for this node
|
|
92
|
+
source_lines = lines[start_line:end_line]
|
|
93
|
+
|
|
94
|
+
# Filter out pysealer decorator lines
|
|
95
|
+
filtered_lines = []
|
|
96
|
+
for line in source_lines:
|
|
97
|
+
stripped = line.strip()
|
|
98
|
+
# Skip lines that are pysealer decorators
|
|
99
|
+
if stripped.startswith('@pysealer.') or stripped.startswith('@pysealer'):
|
|
100
|
+
continue
|
|
101
|
+
filtered_lines.append(line)
|
|
102
|
+
|
|
103
|
+
function_source = '\n'.join(filtered_lines)
|
|
104
|
+
|
|
105
|
+
try:
|
|
106
|
+
private_key = get_private_key()
|
|
107
|
+
except (FileNotFoundError, ValueError) as e:
|
|
108
|
+
raise RuntimeError(f"Cannot add decorators: {e}. Please run 'pysealer init' first.")
|
|
109
|
+
|
|
110
|
+
try:
|
|
111
|
+
signature = generate_signature(function_source, private_key)
|
|
112
|
+
except Exception as e:
|
|
113
|
+
raise RuntimeError(f"Failed to generate signature: {e}")
|
|
114
|
+
|
|
115
|
+
decorator_line = node.lineno - 1
|
|
116
|
+
if hasattr(node, 'decorator_list') and node.decorator_list:
|
|
117
|
+
decorator_line = node.decorator_list[0].lineno - 1
|
|
118
|
+
|
|
119
|
+
decorators_to_add.append((decorator_line, node.col_offset, signature))
|
|
120
|
+
|
|
121
|
+
# If no decorators to add, return original content
|
|
122
|
+
if not decorators_to_add:
|
|
123
|
+
return content, False
|
|
124
|
+
|
|
125
|
+
# Sort in reverse order to add from bottom to top (preserves line numbers)
|
|
126
|
+
decorators_to_add.sort(reverse=True)
|
|
127
|
+
|
|
128
|
+
# Add decorators to the lines first
|
|
129
|
+
for line_idx, col_offset, signature in decorators_to_add:
|
|
130
|
+
indent = ' ' * col_offset
|
|
131
|
+
decorator_line = f"{indent}@pysealer._{signature}()"
|
|
132
|
+
lines.insert(line_idx, decorator_line)
|
|
133
|
+
|
|
134
|
+
# Now add 'import pysealer' at the top if not present
|
|
135
|
+
has_import_pysealer = any(
|
|
136
|
+
line.strip() == 'import pysealer' or line.strip().startswith('import pysealer') or line.strip().startswith('from pysealer')
|
|
137
|
+
for line in lines
|
|
138
|
+
)
|
|
139
|
+
if not has_import_pysealer:
|
|
140
|
+
# Find the import block
|
|
141
|
+
import_indices = [i for i, line in enumerate(lines) if line.strip().startswith('import ') or line.strip().startswith('from ')]
|
|
142
|
+
if import_indices:
|
|
143
|
+
# Insert after the last import in the block
|
|
144
|
+
last_import = import_indices[-1]
|
|
145
|
+
lines.insert(last_import + 1, 'import pysealer')
|
|
146
|
+
else:
|
|
147
|
+
# No import block found, insert after shebang/docstring/comments
|
|
148
|
+
insert_at = 0
|
|
149
|
+
if lines and lines[0].startswith('#!'):
|
|
150
|
+
insert_at = 1
|
|
151
|
+
# Skip module-level docstrings and blank lines
|
|
152
|
+
while insert_at < len(lines):
|
|
153
|
+
line = lines[insert_at].strip()
|
|
154
|
+
if line == '':
|
|
155
|
+
insert_at += 1
|
|
156
|
+
elif line.startswith('"""') or line.startswith("'''"):
|
|
157
|
+
# Handle multi-line docstrings
|
|
158
|
+
quote = '"""' if line.startswith('"""') else "'''"
|
|
159
|
+
# Check if docstring ends on same line
|
|
160
|
+
if line.count(quote) >= 2:
|
|
161
|
+
insert_at += 1
|
|
162
|
+
else:
|
|
163
|
+
# Multi-line docstring
|
|
164
|
+
insert_at += 1
|
|
165
|
+
while insert_at < len(lines) and quote not in lines[insert_at]:
|
|
166
|
+
insert_at += 1
|
|
167
|
+
if insert_at < len(lines):
|
|
168
|
+
insert_at += 1
|
|
169
|
+
elif line.startswith('#'):
|
|
170
|
+
# Skip comments
|
|
171
|
+
insert_at += 1
|
|
172
|
+
else:
|
|
173
|
+
# Found first non-blank, non-comment, non-docstring line
|
|
174
|
+
break
|
|
175
|
+
|
|
176
|
+
lines.insert(insert_at, 'import pysealer')
|
|
177
|
+
# Add blank line after import if the next line isn't blank
|
|
178
|
+
if insert_at + 1 < len(lines) and lines[insert_at + 1].strip() != '':
|
|
179
|
+
lines.insert(insert_at + 1, '')
|
|
180
|
+
|
|
181
|
+
# Join lines back together
|
|
182
|
+
modified_code = '\n'.join(lines)
|
|
183
|
+
|
|
184
|
+
return modified_code, True
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def add_decorators_to_folder(folder_path: str) -> list[str]:
|
|
188
|
+
"""
|
|
189
|
+
Add decorators to all Python files in a folder.
|
|
190
|
+
|
|
191
|
+
Args:
|
|
192
|
+
folder_path: Path to the folder containing Python files
|
|
193
|
+
|
|
194
|
+
Returns:
|
|
195
|
+
List of file paths where decorators were successfully added
|
|
196
|
+
"""
|
|
197
|
+
folder = Path(folder_path)
|
|
198
|
+
|
|
199
|
+
if not folder.exists():
|
|
200
|
+
raise FileNotFoundError(f"Folder '{folder_path}' does not exist.")
|
|
201
|
+
|
|
202
|
+
if not folder.is_dir():
|
|
203
|
+
raise NotADirectoryError(f"'{folder_path}' is not a directory.")
|
|
204
|
+
|
|
205
|
+
# Find all Python files in the folder (recursive)
|
|
206
|
+
python_files = list(folder.rglob('*.py'))
|
|
207
|
+
|
|
208
|
+
if not python_files:
|
|
209
|
+
raise ValueError(f"No Python files found in '{folder_path}'.")
|
|
210
|
+
|
|
211
|
+
decorated_files = []
|
|
212
|
+
errors = []
|
|
213
|
+
|
|
214
|
+
for py_file in python_files:
|
|
215
|
+
try:
|
|
216
|
+
modified_code, has_changes = add_decorators(str(py_file))
|
|
217
|
+
if has_changes:
|
|
218
|
+
with open(py_file, 'w') as f:
|
|
219
|
+
f.write(modified_code)
|
|
220
|
+
decorated_files.append(str(py_file))
|
|
221
|
+
except Exception as e:
|
|
222
|
+
errors.append((str(py_file), str(e)))
|
|
223
|
+
|
|
224
|
+
if errors:
|
|
225
|
+
error_msg = "\n".join([f" - {file}: {error}" for file, error in errors])
|
|
226
|
+
raise RuntimeError(f"Failed to decorate some files:\n{error_msg}")
|
|
227
|
+
|
|
228
|
+
return decorated_files
|
|
@@ -0,0 +1,186 @@
|
|
|
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, List, Tuple, Optional
|
|
7
|
+
from pysealer import verify_signature
|
|
8
|
+
from .setup import get_public_key
|
|
9
|
+
from .git_diff import get_function_diff, is_git_available
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def check_decorators(file_path: str) -> Dict[str, dict]:
|
|
13
|
+
"""
|
|
14
|
+
Parse a Python file and verify all pysealer cryptographic decorators.
|
|
15
|
+
|
|
16
|
+
This function checks that each function/class with a pysealer decorator has a valid
|
|
17
|
+
signature that matches the current source code of that function/class.
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
file_path: Path to the Python file to verify
|
|
21
|
+
|
|
22
|
+
Returns:
|
|
23
|
+
Dictionary mapping function/class names to their verification results:
|
|
24
|
+
{
|
|
25
|
+
"function_name": {
|
|
26
|
+
"valid": bool, # Whether signature is valid
|
|
27
|
+
"signature": str, # The signature found in decorator
|
|
28
|
+
"message": str, # Success or error message
|
|
29
|
+
"has_decorator": bool, # Whether function has pysealer decorator
|
|
30
|
+
"line_start": int, # Starting line number
|
|
31
|
+
"line_end": int, # Ending line number
|
|
32
|
+
"source": str, # Function source code
|
|
33
|
+
"diff": List[Tuple] # Git diff if validation failed
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
"""
|
|
37
|
+
# Read the file content
|
|
38
|
+
with open(file_path, 'r') as f:
|
|
39
|
+
content = f.read()
|
|
40
|
+
|
|
41
|
+
# Parse the Python source code into an AST
|
|
42
|
+
tree = ast.parse(content)
|
|
43
|
+
|
|
44
|
+
# Get the public key for verification
|
|
45
|
+
try:
|
|
46
|
+
public_key = get_public_key()
|
|
47
|
+
except (FileNotFoundError, ValueError) as e:
|
|
48
|
+
raise RuntimeError(f"Cannot verify decorators: {e}. Please run 'pysealer init' first.")
|
|
49
|
+
|
|
50
|
+
# Dictionary to store results
|
|
51
|
+
results = {}
|
|
52
|
+
|
|
53
|
+
# Iterate through each node in the AST
|
|
54
|
+
for node in ast.walk(tree):
|
|
55
|
+
node_type = type(node).__name__
|
|
56
|
+
|
|
57
|
+
# Check if this node is a function or class definition
|
|
58
|
+
if node_type in ("FunctionDef", "AsyncFunctionDef", "ClassDef"):
|
|
59
|
+
name = node.name
|
|
60
|
+
|
|
61
|
+
# Look for pysealer decorator
|
|
62
|
+
signature_from_decorator = None
|
|
63
|
+
has_pysealer_decorator = False
|
|
64
|
+
|
|
65
|
+
if hasattr(node, 'decorator_list'):
|
|
66
|
+
for decorator in node.decorator_list:
|
|
67
|
+
# Check if decorator is a Call node (e.g., @pysealer._<signature>())
|
|
68
|
+
if isinstance(decorator, ast.Call):
|
|
69
|
+
func = decorator.func
|
|
70
|
+
# Check if it's pysealer._<signature>
|
|
71
|
+
if isinstance(func, ast.Attribute):
|
|
72
|
+
if isinstance(func.value, ast.Name) and func.value.id == "pysealer":
|
|
73
|
+
attr_name = func.attr
|
|
74
|
+
# Extract signature (remove leading underscore)
|
|
75
|
+
if attr_name.startswith('_'):
|
|
76
|
+
signature_from_decorator = attr_name[1:]
|
|
77
|
+
has_pysealer_decorator = True
|
|
78
|
+
break
|
|
79
|
+
|
|
80
|
+
# Initialize result for this function/class
|
|
81
|
+
result = {
|
|
82
|
+
"has_decorator": has_pysealer_decorator,
|
|
83
|
+
"valid": False,
|
|
84
|
+
"signature": signature_from_decorator,
|
|
85
|
+
"message": "",
|
|
86
|
+
"line_start": node.lineno,
|
|
87
|
+
"line_end": node.end_lineno if hasattr(node, 'end_lineno') and node.end_lineno else node.lineno,
|
|
88
|
+
"source": "",
|
|
89
|
+
"diff": None
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if not has_pysealer_decorator:
|
|
93
|
+
result["message"] = "No pysealer decorator found"
|
|
94
|
+
results[name] = result
|
|
95
|
+
continue
|
|
96
|
+
|
|
97
|
+
# Extract the source code without pysealer decorators for verification
|
|
98
|
+
# Use original source to preserve formatting (quotes, spacing, etc.)
|
|
99
|
+
content_lines = content.split('\n')
|
|
100
|
+
start_line = node.lineno - 1
|
|
101
|
+
end_line = node.end_lineno if hasattr(node, 'end_lineno') and node.end_lineno else node.lineno
|
|
102
|
+
|
|
103
|
+
# Get the source lines for this node
|
|
104
|
+
source_lines = content_lines[start_line:end_line]
|
|
105
|
+
|
|
106
|
+
# Filter out pysealer decorator lines
|
|
107
|
+
filtered_lines = []
|
|
108
|
+
for line in source_lines:
|
|
109
|
+
stripped = line.strip()
|
|
110
|
+
# Skip lines that are pysealer decorators
|
|
111
|
+
if stripped.startswith('@pysealer.') or stripped.startswith('@pysealer'):
|
|
112
|
+
continue
|
|
113
|
+
filtered_lines.append(line)
|
|
114
|
+
|
|
115
|
+
function_source = '\n'.join(filtered_lines)
|
|
116
|
+
|
|
117
|
+
# Store the source code
|
|
118
|
+
result["source"] = function_source
|
|
119
|
+
|
|
120
|
+
# Verify the signature
|
|
121
|
+
try:
|
|
122
|
+
is_valid = verify_signature(function_source, signature_from_decorator, public_key)
|
|
123
|
+
|
|
124
|
+
result["valid"] = is_valid
|
|
125
|
+
if is_valid:
|
|
126
|
+
result["message"] = "✓ Signature valid - code has not been tampered with"
|
|
127
|
+
else:
|
|
128
|
+
result["message"] = "✗ Signature invalid - code may have been modified"
|
|
129
|
+
|
|
130
|
+
# Try to get git diff for failed validation (only if git is available)
|
|
131
|
+
if is_git_available():
|
|
132
|
+
try:
|
|
133
|
+
diff = get_function_diff(
|
|
134
|
+
file_path,
|
|
135
|
+
name,
|
|
136
|
+
function_source,
|
|
137
|
+
node.lineno
|
|
138
|
+
)
|
|
139
|
+
if diff:
|
|
140
|
+
result["diff"] = diff
|
|
141
|
+
except Exception:
|
|
142
|
+
# If git diff fails, just continue without it
|
|
143
|
+
pass
|
|
144
|
+
|
|
145
|
+
except Exception as e:
|
|
146
|
+
result["message"] = f"✗ Error verifying signature: {e}"
|
|
147
|
+
|
|
148
|
+
results[name] = result
|
|
149
|
+
|
|
150
|
+
return results
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def check_decorators_in_folder(folder_path: str) -> Dict[str, Dict[str, dict]]:
|
|
154
|
+
"""
|
|
155
|
+
Check decorators in all Python files in a folder.
|
|
156
|
+
|
|
157
|
+
Args:
|
|
158
|
+
folder_path: Path to the folder containing Python files
|
|
159
|
+
|
|
160
|
+
Returns:
|
|
161
|
+
Dictionary mapping file paths to their verification results
|
|
162
|
+
"""
|
|
163
|
+
folder = Path(folder_path)
|
|
164
|
+
|
|
165
|
+
if not folder.exists():
|
|
166
|
+
raise FileNotFoundError(f"Folder '{folder_path}' does not exist.")
|
|
167
|
+
|
|
168
|
+
if not folder.is_dir():
|
|
169
|
+
raise NotADirectoryError(f"'{folder_path}' is not a directory.")
|
|
170
|
+
|
|
171
|
+
# Find all Python files in the folder (recursive)
|
|
172
|
+
python_files = list(folder.rglob('*.py'))
|
|
173
|
+
|
|
174
|
+
if not python_files:
|
|
175
|
+
raise ValueError(f"No Python files found in '{folder_path}'.")
|
|
176
|
+
|
|
177
|
+
all_results = {}
|
|
178
|
+
|
|
179
|
+
for py_file in python_files:
|
|
180
|
+
try:
|
|
181
|
+
results = check_decorators(str(py_file))
|
|
182
|
+
all_results[str(py_file)] = results
|
|
183
|
+
except Exception as e:
|
|
184
|
+
all_results[str(py_file)] = {"error": str(e)}
|
|
185
|
+
|
|
186
|
+
return all_results
|