aegis-python-security-scanner 1.0.0__tar.gz

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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Aegis Code
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES, YETCH, OR
19
+ OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, INJURES, OR OTHERWISE,
20
+ ARISING FROM, OUT OF OR THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,81 @@
1
+ Metadata-Version: 2.4
2
+ Name: aegis-python-security-scanner
3
+ Version: 1.0.0
4
+ Summary: A lightweight, local-first Python SAST security scanner
5
+ Home-page: https://github.com/am9024048-hue/Aegis-code
6
+ Author: Ahmed Mohamed
7
+ License: MIT
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: Programming Language :: Python :: 3 :: Only
10
+ Classifier: Topic :: Security
11
+ Requires-Python: >=3.8
12
+ Description-Content-Type: text/markdown
13
+ License-File: LICENSE
14
+ Requires-Dist: rich>=13.0.0
15
+ Dynamic: author
16
+ Dynamic: classifier
17
+ Dynamic: description
18
+ Dynamic: description-content-type
19
+ Dynamic: home-page
20
+ Dynamic: license
21
+ Dynamic: license-file
22
+ Dynamic: requires-dist
23
+ Dynamic: requires-python
24
+ Dynamic: summary
25
+
26
+ # Aegis Code
27
+
28
+ A lightweight, local-first Python static application security testing (SAST)
29
+ scanner. Aegis analyzes Python source with the built-in AST module and checks
30
+ text for likely hardcoded secrets.
31
+
32
+ ## Features
33
+
34
+ - Detects likely hardcoded API keys and tokens.
35
+ - Flags calls to `eval()` and `exec()`.
36
+ - Warns about imports of `pickle`, `telnetlib`, and `xmlrpclib`.
37
+ - Scans source text in memory or reads a Python file from disk.
38
+ - Returns findings as Python objects that can be converted to dictionaries.
39
+
40
+ ## Install
41
+
42
+ From this directory:
43
+
44
+ ```bash
45
+ python -m pip install -e .
46
+ ```
47
+
48
+ This installs the `aegis` package and the `aegis` command.
49
+ The PyPI distribution name is `aegis-python-security-scanner`; the Python
50
+ import name is `aegis`.
51
+
52
+ ## Scan a file
53
+
54
+ ```bash
55
+ aegis path/to/file.py
56
+ ```
57
+
58
+ ## Use the Python API
59
+
60
+ ```python
61
+ from aegis import AegisEngine
62
+
63
+ engine = AegisEngine()
64
+ findings = engine.scan_code("result = eval(user_input)", file_path="example.py")
65
+
66
+ for finding in findings:
67
+ print(finding.to_dict())
68
+ ```
69
+
70
+ `scan_file()` scans a path on disk. `scan_code()` scans a string in memory.
71
+ Findings expose `file_path`, `line`, `severity`, `category`, and `description`.
72
+
73
+ ## Run tests
74
+
75
+ ```bash
76
+ PYTHONPATH=src python -m unittest discover -s tests
77
+ ```
78
+
79
+ ## License
80
+
81
+ MIT. See `LICENSE`.
@@ -0,0 +1,56 @@
1
+ # Aegis Code
2
+
3
+ A lightweight, local-first Python static application security testing (SAST)
4
+ scanner. Aegis analyzes Python source with the built-in AST module and checks
5
+ text for likely hardcoded secrets.
6
+
7
+ ## Features
8
+
9
+ - Detects likely hardcoded API keys and tokens.
10
+ - Flags calls to `eval()` and `exec()`.
11
+ - Warns about imports of `pickle`, `telnetlib`, and `xmlrpclib`.
12
+ - Scans source text in memory or reads a Python file from disk.
13
+ - Returns findings as Python objects that can be converted to dictionaries.
14
+
15
+ ## Install
16
+
17
+ From this directory:
18
+
19
+ ```bash
20
+ python -m pip install -e .
21
+ ```
22
+
23
+ This installs the `aegis` package and the `aegis` command.
24
+ The PyPI distribution name is `aegis-python-security-scanner`; the Python
25
+ import name is `aegis`.
26
+
27
+ ## Scan a file
28
+
29
+ ```bash
30
+ aegis path/to/file.py
31
+ ```
32
+
33
+ ## Use the Python API
34
+
35
+ ```python
36
+ from aegis import AegisEngine
37
+
38
+ engine = AegisEngine()
39
+ findings = engine.scan_code("result = eval(user_input)", file_path="example.py")
40
+
41
+ for finding in findings:
42
+ print(finding.to_dict())
43
+ ```
44
+
45
+ `scan_file()` scans a path on disk. `scan_code()` scans a string in memory.
46
+ Findings expose `file_path`, `line`, `severity`, `category`, and `description`.
47
+
48
+ ## Run tests
49
+
50
+ ```bash
51
+ PYTHONPATH=src python -m unittest discover -s tests
52
+ ```
53
+
54
+ ## License
55
+
56
+ MIT. See `LICENSE`.
@@ -0,0 +1,3 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61.0", "wheel"]
3
+ build-backend = "setuptools.build_meta"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,32 @@
1
+ from pathlib import Path
2
+
3
+ from setuptools import find_packages, setup
4
+
5
+
6
+ ROOT = Path(__file__).parent
7
+
8
+ setup(
9
+ name="aegis-python-security-scanner",
10
+ version="1.0.0",
11
+ description="A lightweight, local-first Python SAST security scanner",
12
+ author="Ahmed Mohamed",
13
+ url="https://github.com/am9024048-hue/Aegis-code",
14
+ license="MIT",
15
+ license_files=["LICENSE"],
16
+ long_description=(ROOT / "README.md").read_text(encoding="utf-8"),
17
+ long_description_content_type="text/markdown",
18
+ package_dir={"": "src"},
19
+ packages=find_packages(where="src"),
20
+ python_requires=">=3.8",
21
+ install_requires=["rich>=13.0.0"],
22
+ entry_points={
23
+ "console_scripts": [
24
+ "aegis=aegis.scanner:main",
25
+ ],
26
+ },
27
+ classifiers=[
28
+ "Programming Language :: Python :: 3",
29
+ "Programming Language :: Python :: 3 :: Only",
30
+ "Topic :: Security",
31
+ ],
32
+ )
@@ -0,0 +1,21 @@
1
+ """Aegis Code: a lightweight Python security scanner."""
2
+
3
+ from .scanner import (
4
+ AegisEngine,
5
+ HardcodedSecretsRule,
6
+ InsecureFunctionsRule,
7
+ InsecureImportsRule,
8
+ RichReporter,
9
+ SecurityRule,
10
+ Vulnerability,
11
+ )
12
+
13
+ __all__ = [
14
+ "AegisEngine",
15
+ "HardcodedSecretsRule",
16
+ "InsecureFunctionsRule",
17
+ "InsecureImportsRule",
18
+ "RichReporter",
19
+ "SecurityRule",
20
+ "Vulnerability",
21
+ ]
@@ -0,0 +1,268 @@
1
+ import ast
2
+ import re
3
+ import sys
4
+ from pathlib import Path
5
+ from typing import Any, Dict, List
6
+
7
+ from rich.console import Console
8
+ from rich.panel import Panel
9
+ from rich.table import Table
10
+ from rich.text import Text
11
+
12
+
13
+ class Vulnerability:
14
+ """Represents a single detected security issue."""
15
+
16
+ def __init__(
17
+ self,
18
+ file_path: str,
19
+ line: int,
20
+ severity: str,
21
+ category: str,
22
+ description: str,
23
+ ):
24
+ self.file_path = file_path
25
+ self.line = line
26
+ self.severity = severity
27
+ self.category = category
28
+ self.description = description
29
+
30
+ def to_dict(self) -> Dict[str, Any]:
31
+ """Convert the finding to a dictionary."""
32
+ return {
33
+ "file_path": self.file_path,
34
+ "line": self.line,
35
+ "severity": self.severity,
36
+ "category": self.category,
37
+ "description": self.description,
38
+ }
39
+
40
+
41
+ class SecurityRule:
42
+ """Base class for all security rules."""
43
+
44
+ def analyze_ast(self, tree: ast.AST, file_path: str) -> List[Vulnerability]:
45
+ return []
46
+
47
+ def analyze_regex(self, content: str, file_path: str) -> List[Vulnerability]:
48
+ return []
49
+
50
+
51
+ class HardcodedSecretsRule(SecurityRule):
52
+ """Detect exposed API keys and sensitive tokens using regular expressions."""
53
+
54
+ PATTERNS = [
55
+ (
56
+ r'(?i)(api[_-]?key|secret[_-]?key|token|auth[_-]?token)\s*=\s*[\'"][A-Za-z0-9_\-]{16,}[\'"]',
57
+ "Potential hardcoded API secret / token detected",
58
+ ),
59
+ (
60
+ r"AIzaSy[A-Za-z0-9_\-]{33}",
61
+ "Google API Key detected",
62
+ ),
63
+ (
64
+ r"sk-[A-Za-z0-9]{32,}",
65
+ "OpenAI / API Secret Key detected",
66
+ ),
67
+ ]
68
+
69
+ def analyze_regex(self, content: str, file_path: str) -> List[Vulnerability]:
70
+ vulnerabilities = []
71
+ for line_number, line in enumerate(content.splitlines(), 1):
72
+ for pattern, description in self.PATTERNS:
73
+ if re.search(pattern, line):
74
+ vulnerabilities.append(
75
+ Vulnerability(
76
+ file_path,
77
+ line_number,
78
+ "HIGH",
79
+ "Hardcoded Secret",
80
+ description,
81
+ )
82
+ )
83
+ return vulnerabilities
84
+
85
+
86
+ class InsecureFunctionsRule(SecurityRule):
87
+ """Detect dangerous dynamic code execution such as eval() and exec()."""
88
+
89
+ def analyze_ast(self, tree: ast.AST, file_path: str) -> List[Vulnerability]:
90
+ vulnerabilities = []
91
+ for node in ast.walk(tree):
92
+ if isinstance(node, ast.Call) and isinstance(node.func, ast.Name):
93
+ if node.func.id in {"eval", "exec"}:
94
+ vulnerabilities.append(
95
+ Vulnerability(
96
+ file_path,
97
+ node.lineno,
98
+ "HIGH",
99
+ "Dynamic Execution",
100
+ f"Use of dangerous function '{node.func.id}' detected",
101
+ )
102
+ )
103
+ return vulnerabilities
104
+
105
+
106
+ class InsecureImportsRule(SecurityRule):
107
+ """Warn about imports of insecure or deprecated modules."""
108
+
109
+ INSECURE_MODULES = {"pickle", "telnetlib", "xmlrpclib"}
110
+
111
+ def analyze_ast(self, tree: ast.AST, file_path: str) -> List[Vulnerability]:
112
+ vulnerabilities = []
113
+ for node in ast.walk(tree):
114
+ if isinstance(node, ast.Import):
115
+ for alias in node.names:
116
+ if alias.name in self.INSECURE_MODULES:
117
+ vulnerabilities.append(
118
+ Vulnerability(
119
+ file_path,
120
+ node.lineno,
121
+ "MEDIUM",
122
+ "Insecure Import",
123
+ f"Import of insecure module '{alias.name}' detected",
124
+ )
125
+ )
126
+ elif isinstance(node, ast.ImportFrom):
127
+ if node.module in self.INSECURE_MODULES:
128
+ vulnerabilities.append(
129
+ Vulnerability(
130
+ file_path,
131
+ node.lineno,
132
+ "MEDIUM",
133
+ "Insecure Import",
134
+ f"Import from insecure module '{node.module}' detected",
135
+ )
136
+ )
137
+ return vulnerabilities
138
+
139
+
140
+ class AegisEngine:
141
+ """Core scanner, independent of the CLI and terminal reporter."""
142
+
143
+ def __init__(self):
144
+ self.rules: List[SecurityRule] = [
145
+ HardcodedSecretsRule(),
146
+ InsecureFunctionsRule(),
147
+ InsecureImportsRule(),
148
+ ]
149
+
150
+ def scan_code(
151
+ self,
152
+ content: str,
153
+ file_path: str = "snippet.py",
154
+ ) -> List[Vulnerability]:
155
+ """Scan source code held in memory."""
156
+ findings = []
157
+ try:
158
+ tree = ast.parse(content, filename=file_path)
159
+ for rule in self.rules:
160
+ findings.extend(rule.analyze_ast(tree, file_path))
161
+ except SyntaxError as error:
162
+ findings.append(
163
+ Vulnerability(
164
+ file_path,
165
+ error.lineno or 1,
166
+ "HIGH",
167
+ "Syntax Error",
168
+ f"Unable to parse file: {error.msg}",
169
+ )
170
+ )
171
+
172
+ for rule in self.rules:
173
+ findings.extend(rule.analyze_regex(content, file_path))
174
+
175
+ return findings
176
+
177
+ def scan_file(self, target_path: Path) -> List[Vulnerability]:
178
+ """Scan a Python source file from disk."""
179
+ if not target_path.exists():
180
+ return [
181
+ Vulnerability(
182
+ str(target_path),
183
+ 0,
184
+ "HIGH",
185
+ "File Error",
186
+ "File does not exist",
187
+ )
188
+ ]
189
+
190
+ try:
191
+ content = target_path.read_text(encoding="utf-8")
192
+ return self.scan_code(content, str(target_path))
193
+ except Exception as error:
194
+ return [
195
+ Vulnerability(
196
+ str(target_path),
197
+ 0,
198
+ "HIGH",
199
+ "Read Error",
200
+ f"Failed to read file: {error}",
201
+ )
202
+ ]
203
+
204
+
205
+ class RichReporter:
206
+ """Render scan results in the terminal using Rich."""
207
+
208
+ def __init__(self):
209
+ self.console = Console()
210
+
211
+ def print_banner(self):
212
+ banner_text = Text("AEGIS CODE - SAST Security Scanner", style="bold cyan")
213
+ self.console.print(Panel(banner_text, border_style="blue", expand=False))
214
+
215
+ def render_report(self, findings: List[Vulnerability], file_scanned: str):
216
+ self.print_banner()
217
+ self.console.print(
218
+ f"\n[bold white]Scanned File:[/bold white] "
219
+ f"[yellow]{file_scanned}[/yellow]\n"
220
+ )
221
+
222
+ if not findings:
223
+ self.console.print(
224
+ Panel(
225
+ "[bold green]No security issues detected! "
226
+ "Your code looks clean.[/bold green]",
227
+ border_style="green",
228
+ )
229
+ )
230
+ return
231
+
232
+ table = Table(title="Security Findings", show_header=True, header_style="bold magenta")
233
+ table.add_column("Line", style="dim", width=8, justify="center")
234
+ table.add_column("Severity", width=12, justify="center")
235
+ table.add_column("Category", width=20)
236
+ table.add_column("Description", style="white")
237
+
238
+ for finding in findings:
239
+ severity_style = "bold red" if finding.severity == "HIGH" else "bold yellow"
240
+ severity_text = (
241
+ f"[{severity_style}]{finding.severity}[/{severity_style}]"
242
+ )
243
+ table.add_row(
244
+ str(finding.line),
245
+ severity_text,
246
+ finding.category,
247
+ finding.description,
248
+ )
249
+
250
+ self.console.print(table)
251
+ self.console.print(
252
+ f"\n[bold red]Total Issues Found: {len(findings)}[/bold red]\n"
253
+ )
254
+
255
+
256
+ def main():
257
+ """Run the file-scanning command-line interface."""
258
+ if len(sys.argv) < 2:
259
+ Console().print(
260
+ "[bold yellow]Usage:[/bold yellow] aegis <path_to_python_file>"
261
+ )
262
+ sys.exit(1)
263
+
264
+ target_file = Path(sys.argv[1])
265
+ engine = AegisEngine()
266
+ reporter = RichReporter()
267
+ findings = engine.scan_file(target_file)
268
+ reporter.render_report(findings, str(target_file))
@@ -0,0 +1,81 @@
1
+ Metadata-Version: 2.4
2
+ Name: aegis-python-security-scanner
3
+ Version: 1.0.0
4
+ Summary: A lightweight, local-first Python SAST security scanner
5
+ Home-page: https://github.com/am9024048-hue/Aegis-code
6
+ Author: Ahmed Mohamed
7
+ License: MIT
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: Programming Language :: Python :: 3 :: Only
10
+ Classifier: Topic :: Security
11
+ Requires-Python: >=3.8
12
+ Description-Content-Type: text/markdown
13
+ License-File: LICENSE
14
+ Requires-Dist: rich>=13.0.0
15
+ Dynamic: author
16
+ Dynamic: classifier
17
+ Dynamic: description
18
+ Dynamic: description-content-type
19
+ Dynamic: home-page
20
+ Dynamic: license
21
+ Dynamic: license-file
22
+ Dynamic: requires-dist
23
+ Dynamic: requires-python
24
+ Dynamic: summary
25
+
26
+ # Aegis Code
27
+
28
+ A lightweight, local-first Python static application security testing (SAST)
29
+ scanner. Aegis analyzes Python source with the built-in AST module and checks
30
+ text for likely hardcoded secrets.
31
+
32
+ ## Features
33
+
34
+ - Detects likely hardcoded API keys and tokens.
35
+ - Flags calls to `eval()` and `exec()`.
36
+ - Warns about imports of `pickle`, `telnetlib`, and `xmlrpclib`.
37
+ - Scans source text in memory or reads a Python file from disk.
38
+ - Returns findings as Python objects that can be converted to dictionaries.
39
+
40
+ ## Install
41
+
42
+ From this directory:
43
+
44
+ ```bash
45
+ python -m pip install -e .
46
+ ```
47
+
48
+ This installs the `aegis` package and the `aegis` command.
49
+ The PyPI distribution name is `aegis-python-security-scanner`; the Python
50
+ import name is `aegis`.
51
+
52
+ ## Scan a file
53
+
54
+ ```bash
55
+ aegis path/to/file.py
56
+ ```
57
+
58
+ ## Use the Python API
59
+
60
+ ```python
61
+ from aegis import AegisEngine
62
+
63
+ engine = AegisEngine()
64
+ findings = engine.scan_code("result = eval(user_input)", file_path="example.py")
65
+
66
+ for finding in findings:
67
+ print(finding.to_dict())
68
+ ```
69
+
70
+ `scan_file()` scans a path on disk. `scan_code()` scans a string in memory.
71
+ Findings expose `file_path`, `line`, `severity`, `category`, and `description`.
72
+
73
+ ## Run tests
74
+
75
+ ```bash
76
+ PYTHONPATH=src python -m unittest discover -s tests
77
+ ```
78
+
79
+ ## License
80
+
81
+ MIT. See `LICENSE`.
@@ -0,0 +1,13 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ setup.py
5
+ src/aegis/__init__.py
6
+ src/aegis/scanner.py
7
+ src/aegis_python_security_scanner.egg-info/PKG-INFO
8
+ src/aegis_python_security_scanner.egg-info/SOURCES.txt
9
+ src/aegis_python_security_scanner.egg-info/dependency_links.txt
10
+ src/aegis_python_security_scanner.egg-info/entry_points.txt
11
+ src/aegis_python_security_scanner.egg-info/requires.txt
12
+ src/aegis_python_security_scanner.egg-info/top_level.txt
13
+ tests/test_package.py
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ aegis = aegis.scanner:main
@@ -0,0 +1,62 @@
1
+ import unittest
2
+
3
+ from aegis import AegisEngine, Vulnerability
4
+
5
+
6
+ class AegisEngineTests(unittest.TestCase):
7
+ def setUp(self):
8
+ self.engine = AegisEngine()
9
+
10
+ def test_finds_dynamic_execution(self):
11
+ findings = self.engine.scan_code("result = eval(user_input)", "sample.py")
12
+
13
+ self.assertTrue(
14
+ any(
15
+ finding.category == "Dynamic Execution"
16
+ and finding.severity == "HIGH"
17
+ and finding.line == 1
18
+ for finding in findings
19
+ )
20
+ )
21
+
22
+ def test_finds_insecure_imports(self):
23
+ findings = self.engine.scan_code("import pickle", "sample.py")
24
+
25
+ self.assertTrue(
26
+ any(finding.category == "Insecure Import" for finding in findings)
27
+ )
28
+
29
+ def test_finds_hardcoded_secrets(self):
30
+ findings = self.engine.scan_code(
31
+ 'api_key = "abcdefghijklmnop"',
32
+ "sample.py",
33
+ )
34
+
35
+ self.assertTrue(
36
+ any(finding.category == "Hardcoded Secret" for finding in findings)
37
+ )
38
+
39
+ def test_reports_syntax_errors(self):
40
+ findings = self.engine.scan_code("if:", "sample.py")
41
+
42
+ self.assertTrue(
43
+ any(finding.category == "Syntax Error" for finding in findings)
44
+ )
45
+
46
+ def test_finding_serializes_to_a_dictionary(self):
47
+ finding = Vulnerability("sample.py", 3, "LOW", "Example", "Example finding")
48
+
49
+ self.assertEqual(
50
+ finding.to_dict(),
51
+ {
52
+ "file_path": "sample.py",
53
+ "line": 3,
54
+ "severity": "LOW",
55
+ "category": "Example",
56
+ "description": "Example finding",
57
+ },
58
+ )
59
+
60
+
61
+ if __name__ == "__main__":
62
+ unittest.main()