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.
- aegis_python_security_scanner-1.0.0/LICENSE +21 -0
- aegis_python_security_scanner-1.0.0/PKG-INFO +81 -0
- aegis_python_security_scanner-1.0.0/README.md +56 -0
- aegis_python_security_scanner-1.0.0/pyproject.toml +3 -0
- aegis_python_security_scanner-1.0.0/setup.cfg +4 -0
- aegis_python_security_scanner-1.0.0/setup.py +32 -0
- aegis_python_security_scanner-1.0.0/src/aegis/__init__.py +21 -0
- aegis_python_security_scanner-1.0.0/src/aegis/scanner.py +268 -0
- aegis_python_security_scanner-1.0.0/src/aegis_python_security_scanner.egg-info/PKG-INFO +81 -0
- aegis_python_security_scanner-1.0.0/src/aegis_python_security_scanner.egg-info/SOURCES.txt +13 -0
- aegis_python_security_scanner-1.0.0/src/aegis_python_security_scanner.egg-info/dependency_links.txt +1 -0
- aegis_python_security_scanner-1.0.0/src/aegis_python_security_scanner.egg-info/entry_points.txt +2 -0
- aegis_python_security_scanner-1.0.0/src/aegis_python_security_scanner.egg-info/requires.txt +1 -0
- aegis_python_security_scanner-1.0.0/src/aegis_python_security_scanner.egg-info/top_level.txt +1 -0
- aegis_python_security_scanner-1.0.0/tests/test_package.py +62 -0
|
@@ -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,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
|
aegis_python_security_scanner-1.0.0/src/aegis_python_security_scanner.egg-info/dependency_links.txt
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
rich>=13.0.0
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
aegis
|
|
@@ -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()
|