patchops 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.
- patchops-1.0.0/MANIFEST.in +4 -0
- patchops-1.0.0/PKG-INFO +17 -0
- patchops-1.0.0/PatchOps-Target/__pycache__/auth.cpython-311.pyc +0 -0
- patchops-1.0.0/PatchOps-Target/__pycache__/config.cpython-311.pyc +0 -0
- patchops-1.0.0/PatchOps-Target/__pycache__/db_utils.cpython-311.pyc +0 -0
- patchops-1.0.0/PatchOps-Target/__pycache__/init_db.cpython-311.pyc +0 -0
- patchops-1.0.0/PatchOps-Target/__pycache__/reports.cpython-311.pyc +0 -0
- patchops-1.0.0/PatchOps-Target/analytics.py +2 -0
- patchops-1.0.0/PatchOps-Target/api_client.py +4 -0
- patchops-1.0.0/PatchOps-Target/app.py +52 -0
- patchops-1.0.0/PatchOps-Target/auth.py +20 -0
- patchops-1.0.0/PatchOps-Target/cache_manager.py +7 -0
- patchops-1.0.0/PatchOps-Target/config.py +6 -0
- patchops-1.0.0/PatchOps-Target/db_utils.py +20 -0
- patchops-1.0.0/PatchOps-Target/email_service.py +2 -0
- patchops-1.0.0/PatchOps-Target/file_manager.py +4 -0
- patchops-1.0.0/PatchOps-Target/init_db.py +17 -0
- patchops-1.0.0/PatchOps-Target/logger.py +4 -0
- patchops-1.0.0/PatchOps-Target/payment_gateway.py +2 -0
- patchops-1.0.0/PatchOps-Target/reports.py +9 -0
- patchops-1.0.0/PatchOps-Target/search_engine.py +4 -0
- patchops-1.0.0/PatchOps-Target/tests/smoke_test.py +48 -0
- patchops-1.0.0/PatchOps-Target/tests/test_suite.py +9 -0
- patchops-1.0.0/PatchOps-Target/users.db +0 -0
- patchops-1.0.0/PatchOps-Target/utils.py +15 -0
- patchops-1.0.0/PatchOps-Target/validator.py +4 -0
- patchops-1.0.0/README.md +1 -0
- patchops-1.0.0/frontend/README.md +134 -0
- patchops-1.0.0/frontend/index.html +522 -0
- patchops-1.0.0/frontend/script.js +287 -0
- patchops-1.0.0/frontend/server.py +116 -0
- patchops-1.0.0/frontend/style.css +263 -0
- patchops-1.0.0/lambdas/__init__.py +1 -0
- patchops-1.0.0/lambdas/__pycache__/__init__.cpython-311.pyc +0 -0
- patchops-1.0.0/lambdas/code_analyzer/__init__.py +1 -0
- patchops-1.0.0/lambdas/code_analyzer/__pycache__/__init__.cpython-311.pyc +0 -0
- patchops-1.0.0/lambdas/code_analyzer/__pycache__/handler.cpython-311.pyc +0 -0
- patchops-1.0.0/lambdas/code_analyzer/handler.py +87 -0
- patchops-1.0.0/lambdas/component_tester/__pycache__/handler.cpython-311.pyc +0 -0
- patchops-1.0.0/lambdas/component_tester/handler.py +47 -0
- patchops-1.0.0/lambdas/exploit_crafter/__init__.py +1 -0
- patchops-1.0.0/lambdas/exploit_crafter/__pycache__/__init__.cpython-311.pyc +0 -0
- patchops-1.0.0/lambdas/exploit_crafter/__pycache__/handler.cpython-311.pyc +0 -0
- patchops-1.0.0/lambdas/exploit_crafter/handler.py +111 -0
- patchops-1.0.0/lambdas/graph_builder/__pycache__/handler.cpython-311.pyc +0 -0
- patchops-1.0.0/lambdas/graph_builder/handler.py +52 -0
- patchops-1.0.0/lambdas/neighbor_resolver/__pycache__/handler.cpython-311.pyc +0 -0
- patchops-1.0.0/lambdas/neighbor_resolver/handler.py +23 -0
- patchops-1.0.0/lambdas/orchestrator/__init__.py +0 -0
- patchops-1.0.0/lambdas/orchestrator/handler.py +33 -0
- patchops-1.0.0/lambdas/patch_writer/__init__.py +0 -0
- patchops-1.0.0/lambdas/patch_writer/__pycache__/__init__.cpython-311.pyc +0 -0
- patchops-1.0.0/lambdas/patch_writer/__pycache__/handler.cpython-311.pyc +0 -0
- patchops-1.0.0/lambdas/patch_writer/handler.py +121 -0
- patchops-1.0.0/lambdas/pr_generator/__init__.py +0 -0
- patchops-1.0.0/lambdas/pr_generator/__pycache__/__init__.cpython-311.pyc +0 -0
- patchops-1.0.0/lambdas/pr_generator/__pycache__/handler.cpython-311.pyc +0 -0
- patchops-1.0.0/lambdas/pr_generator/__pycache__/template.cpython-311.pyc +0 -0
- patchops-1.0.0/lambdas/pr_generator/handler.py +126 -0
- patchops-1.0.0/lambdas/pr_generator/template.py +87 -0
- patchops-1.0.0/lambdas/requirements_checker/__pycache__/handler.cpython-311.pyc +0 -0
- patchops-1.0.0/lambdas/requirements_checker/handler.py +94 -0
- patchops-1.0.0/lambdas/security_reviewer/__init__.py +1 -0
- patchops-1.0.0/lambdas/security_reviewer/__pycache__/__init__.cpython-311.pyc +0 -0
- patchops-1.0.0/lambdas/security_reviewer/__pycache__/handler.cpython-311.pyc +0 -0
- patchops-1.0.0/lambdas/security_reviewer/handler.py +117 -0
- patchops-1.0.0/lambdas/shared/__init__.py +1 -0
- patchops-1.0.0/lambdas/shared/__pycache__/__init__.cpython-311.pyc +0 -0
- patchops-1.0.0/lambdas/shared/__pycache__/utils.cpython-311.pyc +0 -0
- patchops-1.0.0/lambdas/shared/utils.py +123 -0
- patchops-1.0.0/lambdas/system_tester/__pycache__/handler.cpython-311.pyc +0 -0
- patchops-1.0.0/lambdas/system_tester/handler.py +110 -0
- patchops-1.0.0/patchops/__init__.py +1 -0
- patchops-1.0.0/patchops/cli.py +68 -0
- patchops-1.0.0/patchops.egg-info/PKG-INFO +17 -0
- patchops-1.0.0/patchops.egg-info/SOURCES.txt +81 -0
- patchops-1.0.0/patchops.egg-info/dependency_links.txt +1 -0
- patchops-1.0.0/patchops.egg-info/entry_points.txt +2 -0
- patchops-1.0.0/patchops.egg-info/requires.txt +11 -0
- patchops-1.0.0/patchops.egg-info/top_level.txt +2 -0
- patchops-1.0.0/pipeline_api.py +209 -0
- patchops-1.0.0/setup.cfg +4 -0
- patchops-1.0.0/setup.py +27 -0
patchops-1.0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: patchops
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Requires-Python: >=3.9
|
|
5
|
+
Requires-Dist: click
|
|
6
|
+
Requires-Dist: fastapi
|
|
7
|
+
Requires-Dist: uvicorn
|
|
8
|
+
Requires-Dist: sse-starlette
|
|
9
|
+
Requires-Dist: groq
|
|
10
|
+
Requires-Dist: requests
|
|
11
|
+
Requires-Dist: PyGithub
|
|
12
|
+
Requires-Dist: python-dotenv
|
|
13
|
+
Requires-Dist: httpx
|
|
14
|
+
Requires-Dist: flask
|
|
15
|
+
Requires-Dist: gitpython
|
|
16
|
+
Dynamic: requires-dist
|
|
17
|
+
Dynamic: requires-python
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import subprocess
|
|
3
|
+
from flask import Flask, request, jsonify
|
|
4
|
+
import auth
|
|
5
|
+
import config
|
|
6
|
+
import db_utils
|
|
7
|
+
import reports
|
|
8
|
+
|
|
9
|
+
app = Flask(__name__)
|
|
10
|
+
|
|
11
|
+
@app.route('/health')
|
|
12
|
+
def health():
|
|
13
|
+
return jsonify({"status": "ok"})
|
|
14
|
+
|
|
15
|
+
@app.get('/user')
|
|
16
|
+
def get_user():
|
|
17
|
+
# CWE-89 SQL Injection: Directly inserting user input into query string
|
|
18
|
+
user_id = request.args.get('id')
|
|
19
|
+
conn = db_utils.get_connection()
|
|
20
|
+
cursor = conn.cursor()
|
|
21
|
+
# Vulnerable line: f-string insertion
|
|
22
|
+
query = f"SELECT username, email, role FROM users WHERE id = {user_id}"
|
|
23
|
+
cursor.execute(query)
|
|
24
|
+
user = cursor.fetchone()
|
|
25
|
+
conn.close()
|
|
26
|
+
if user:
|
|
27
|
+
return jsonify({"username": user[0], "email": user[1], "role": user[2]})
|
|
28
|
+
return jsonify({"error": "User not found"}), 404
|
|
29
|
+
|
|
30
|
+
@app.get('/ping')
|
|
31
|
+
def ping():
|
|
32
|
+
# CWE-78 Command Injection: Unsafe shell execution of user input
|
|
33
|
+
ip = request.args.get('ip')
|
|
34
|
+
try:
|
|
35
|
+
# Vulnerable line: shell=True and f-string
|
|
36
|
+
output = subprocess.check_output(f"ping -n 1 {ip}", shell=True)
|
|
37
|
+
return jsonify({"output": output.decode()})
|
|
38
|
+
except Exception as e:
|
|
39
|
+
return jsonify({"error": str(e)}), 500
|
|
40
|
+
|
|
41
|
+
@app.get('/profile')
|
|
42
|
+
def profile():
|
|
43
|
+
user_id = request.args.get('id')
|
|
44
|
+
profile_data = auth.get_profile(user_id)
|
|
45
|
+
if profile_data:
|
|
46
|
+
return jsonify({"profile": profile_data})
|
|
47
|
+
return jsonify({"error": "Profile not found"}), 404
|
|
48
|
+
|
|
49
|
+
if __name__ == '__main__':
|
|
50
|
+
import init_db
|
|
51
|
+
init_db.init()
|
|
52
|
+
app.run(port=5000)
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import db_utils
|
|
2
|
+
import config
|
|
3
|
+
|
|
4
|
+
def login(username, password):
|
|
5
|
+
# Mock login
|
|
6
|
+
return {"status": "success", "token": "mock-token-123"}
|
|
7
|
+
|
|
8
|
+
def get_profile(user_id):
|
|
9
|
+
# CWE-639 IDOR: no ownership check, directly queries DB with user-supplied ID
|
|
10
|
+
conn = db_utils.get_connection()
|
|
11
|
+
cursor = conn.cursor()
|
|
12
|
+
# Vulnerable line: direct execution with user_id f-string
|
|
13
|
+
query = f"SELECT * FROM users WHERE id = {user_id}"
|
|
14
|
+
cursor.execute(query)
|
|
15
|
+
profile = cursor.fetchone()
|
|
16
|
+
conn.close()
|
|
17
|
+
return profile
|
|
18
|
+
|
|
19
|
+
def check_token(token):
|
|
20
|
+
return token == "mock-token-123"
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import sqlite3
|
|
2
|
+
import config
|
|
3
|
+
|
|
4
|
+
def get_connection():
|
|
5
|
+
# In a real app, DB_PASSWORD would be used here
|
|
6
|
+
return sqlite3.connect(DB_PATH)
|
|
7
|
+
|
|
8
|
+
def safe_query(query, params=()):
|
|
9
|
+
conn = get_connection()
|
|
10
|
+
cursor = conn.cursor()
|
|
11
|
+
cursor.execute(query, params)
|
|
12
|
+
result = cursor.fetchall()
|
|
13
|
+
conn.close()
|
|
14
|
+
return result
|
|
15
|
+
|
|
16
|
+
def query_user(user_id):
|
|
17
|
+
return safe_query("SELECT * FROM users WHERE id = ?", (user_id,))
|
|
18
|
+
|
|
19
|
+
def query_report(report_id):
|
|
20
|
+
return safe_query("SELECT * FROM reports WHERE id = ?", (report_id,))
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import sqlite3
|
|
2
|
+
import os
|
|
3
|
+
# Removed cross-imports
|
|
4
|
+
|
|
5
|
+
def init(app=None):
|
|
6
|
+
if os.path.exists("users.db"):
|
|
7
|
+
os.remove("users.db")
|
|
8
|
+
conn = sqlite3.connect("users.db")
|
|
9
|
+
cursor = conn.cursor()
|
|
10
|
+
cursor.execute("CREATE TABLE users (id INTEGER PRIMARY KEY, username TEXT, email TEXT, role TEXT, password TEXT)")
|
|
11
|
+
cursor.execute("INSERT INTO users VALUES (1, 'alice', 'alice@example.com', 'admin', 'password123')")
|
|
12
|
+
cursor.execute("INSERT INTO users VALUES (2, 'bob', 'bob@example.com', 'user', 'password456')")
|
|
13
|
+
conn.commit()
|
|
14
|
+
conn.close()
|
|
15
|
+
|
|
16
|
+
if __name__ == '__main__':
|
|
17
|
+
init()
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import requests
|
|
2
|
+
import pytest
|
|
3
|
+
|
|
4
|
+
BASE_URL = "http://localhost:5000"
|
|
5
|
+
|
|
6
|
+
def test_health_endpoint():
|
|
7
|
+
"""Verify patched app is alive."""
|
|
8
|
+
r = requests.get(f"{BASE_URL}/health", timeout=5)
|
|
9
|
+
assert r.status_code == 200
|
|
10
|
+
assert r.json()["status"] == "ok"
|
|
11
|
+
|
|
12
|
+
def test_user_endpoint_returns_data():
|
|
13
|
+
"""Verify /user endpoint still functions."""
|
|
14
|
+
r = requests.get(f"{BASE_URL}/user?id=1", timeout=5)
|
|
15
|
+
assert r.status_code == 200
|
|
16
|
+
# The database initialization creates a users table
|
|
17
|
+
assert "username" in r.json()
|
|
18
|
+
|
|
19
|
+
def test_user_endpoint_no_sqli_bypass():
|
|
20
|
+
"""Security smoke test: verify SQLi payload doesn't dump all users."""
|
|
21
|
+
# Payload that would normally dump more or bypass if not parameterized
|
|
22
|
+
r = requests.get(f"{BASE_URL}/user", params={"id": "1 OR 1=1--"}, timeout=5)
|
|
23
|
+
# If patched correctly, this should either 404 (not found) or return exactly one user (if id '1 OR 1=1--' is treated as a string)
|
|
24
|
+
# or return a 500/400 if it's a strict type check. For this demo, let's check it doesn't return more than 1 user.
|
|
25
|
+
if r.status_code == 200:
|
|
26
|
+
data = r.json()
|
|
27
|
+
# If it returns a single user object, its length (keys) is small.
|
|
28
|
+
# If it returns a list, we check length. Our current endpoint returns a single object.
|
|
29
|
+
assert "username" in data
|
|
30
|
+
else:
|
|
31
|
+
assert r.status_code in [404, 400, 500]
|
|
32
|
+
|
|
33
|
+
def test_profile_endpoint_exists():
|
|
34
|
+
"""Verify /profile exists and doesn't crash."""
|
|
35
|
+
r = requests.get(f"{BASE_URL}/profile?id=1", timeout=5)
|
|
36
|
+
assert r.status_code in [200, 401, 403, 404]
|
|
37
|
+
|
|
38
|
+
def test_ping_endpoint_no_command_injection():
|
|
39
|
+
"""Security smoke test: verify command injection is blocked."""
|
|
40
|
+
r = requests.get(f"{BASE_URL}/ping?ip=127.0.0.1; ls", timeout=5)
|
|
41
|
+
# If patched, the output should just be the ping result for "127.0.0.1; ls" which usually fails or just pings.
|
|
42
|
+
# It shouldn't contain etc/password or file listing markers.
|
|
43
|
+
if r.status_code == 200:
|
|
44
|
+
text = r.json().get("output", "").lower()
|
|
45
|
+
assert "etc" not in text
|
|
46
|
+
assert "root" not in text
|
|
47
|
+
else:
|
|
48
|
+
assert r.status_code in [400, 500]
|
|
Binary file
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from datetime import datetime
|
|
3
|
+
import cache_manager
|
|
4
|
+
import validator
|
|
5
|
+
import logger
|
|
6
|
+
import file_manager
|
|
7
|
+
|
|
8
|
+
def sanitize_string(s):
|
|
9
|
+
return re.sub(r'[^\w\s]', '', s)
|
|
10
|
+
|
|
11
|
+
def validate_email(email):
|
|
12
|
+
return "@" in email
|
|
13
|
+
|
|
14
|
+
def format_date(d):
|
|
15
|
+
return datetime.now().strftime("%Y-%m-%d")
|
patchops-1.0.0/README.md
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
## PatchOps
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
# PatchOps Security Terminal Frontend
|
|
2
|
+
|
|
3
|
+
A hacker-themed terminal interface that displays the PatchOps security analysis pipeline in real-time.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
### 🎨 **Hacker Terminal Theme**
|
|
8
|
+
- Retro green-on-black terminal aesthetic
|
|
9
|
+
- Matrix rain effects
|
|
10
|
+
- Glowing indicators and animations
|
|
11
|
+
- Authentic terminal feel with proper command line
|
|
12
|
+
|
|
13
|
+
### 🔄 **Real-time Pipeline Visualization**
|
|
14
|
+
- **GitHub Fetch**: Shows repository cloning progress
|
|
15
|
+
- **File Reading**: Displays source code scanning
|
|
16
|
+
- **Security Analyzer**: 8-step vulnerability detection process
|
|
17
|
+
- **Vulnerability Results**: Shows discovered security issues
|
|
18
|
+
- **Exploit Simulation**: Demonstrates attack vectors
|
|
19
|
+
- **Patch Generation**: Visualizes security fixes
|
|
20
|
+
- **PR Creation**: Shows pull request generation
|
|
21
|
+
|
|
22
|
+
### 🛡️ **Security Vulnerability Display**
|
|
23
|
+
- Color-coded severity levels (Critical/High)
|
|
24
|
+
- CWE identifiers
|
|
25
|
+
- Real-time status indicators
|
|
26
|
+
- Progress bars for long-running operations
|
|
27
|
+
|
|
28
|
+
### ⚡ **Interactive Features**
|
|
29
|
+
- Command-line interface with `run`, `clear`, and `help` commands
|
|
30
|
+
- Real-time typing effects
|
|
31
|
+
- Smooth animations and transitions
|
|
32
|
+
- Responsive design
|
|
33
|
+
|
|
34
|
+
## Quick Start
|
|
35
|
+
|
|
36
|
+
### Method 1: Using the Built-in Server (Recommended)
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
cd frontend
|
|
40
|
+
python server.py
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
The server will automatically:
|
|
44
|
+
- Start on `http://localhost:8080`
|
|
45
|
+
- Open your default browser
|
|
46
|
+
- Serve the terminal interface
|
|
47
|
+
|
|
48
|
+
### Method 2: Manual File Opening
|
|
49
|
+
|
|
50
|
+
1. Open `frontend/index.html` in your web browser
|
|
51
|
+
2. The terminal will load immediately
|
|
52
|
+
|
|
53
|
+
## Commands
|
|
54
|
+
|
|
55
|
+
Once the terminal is running, you can use these commands:
|
|
56
|
+
|
|
57
|
+
- `run` - Start the security analysis simulation
|
|
58
|
+
- `clear` - Clear the terminal screen
|
|
59
|
+
- `help` - Show available commands
|
|
60
|
+
|
|
61
|
+
## Backend Integration
|
|
62
|
+
|
|
63
|
+
The frontend includes API endpoints for real backend integration:
|
|
64
|
+
|
|
65
|
+
- `GET /api/status` - Get current analysis status
|
|
66
|
+
- `GET /api/analyze` - Trigger real security analysis
|
|
67
|
+
|
|
68
|
+
## Technical Details
|
|
69
|
+
|
|
70
|
+
### Frontend Stack
|
|
71
|
+
- **HTML5**: Semantic structure
|
|
72
|
+
- **CSS3**: Custom animations and effects
|
|
73
|
+
- **Vanilla JavaScript**: No dependencies, pure terminal experience
|
|
74
|
+
|
|
75
|
+
### Key Features
|
|
76
|
+
- **Progressive Enhancement**: Works without backend
|
|
77
|
+
- **Responsive Design**: Adapts to different screen sizes
|
|
78
|
+
- **Accessibility**: Proper ARIA labels and keyboard navigation
|
|
79
|
+
- **Performance**: Optimized animations using CSS transforms
|
|
80
|
+
|
|
81
|
+
### Visual Effects
|
|
82
|
+
- **Matrix Rain**: Background binary rain effect
|
|
83
|
+
- **Typing Animation**: Character-by-character text display
|
|
84
|
+
- **Progress Bars**: Smooth progress visualization
|
|
85
|
+
- **Status Indicators**: Pulsing status lights
|
|
86
|
+
- **Hover Effects**: Interactive element feedback
|
|
87
|
+
|
|
88
|
+
## Customization
|
|
89
|
+
|
|
90
|
+
### Colors
|
|
91
|
+
Edit `style.css` to customize the terminal colors:
|
|
92
|
+
- `--terminal-bg`: Background color
|
|
93
|
+
- `--terminal-text`: Default text color
|
|
94
|
+
- `--terminal-accent`: Accent/highlight color
|
|
95
|
+
|
|
96
|
+
### Animations
|
|
97
|
+
Animation speeds and effects are controlled via CSS keyframes:
|
|
98
|
+
- `@keyframes blink`: Cursor blinking
|
|
99
|
+
- `@keyframes pulse`: Status indicator pulsing
|
|
100
|
+
- `@keyframes scan`: Scan line effect
|
|
101
|
+
|
|
102
|
+
### Content
|
|
103
|
+
Modify `script.js` to customize:
|
|
104
|
+
- Vulnerability types and severities
|
|
105
|
+
- Analysis steps and timing
|
|
106
|
+
- Command responses
|
|
107
|
+
- Terminal welcome message
|
|
108
|
+
|
|
109
|
+
## Browser Compatibility
|
|
110
|
+
|
|
111
|
+
- ✅ Chrome 60+
|
|
112
|
+
- ✅ Firefox 55+
|
|
113
|
+
- ✅ Safari 12+
|
|
114
|
+
- ✅ Edge 79+
|
|
115
|
+
|
|
116
|
+
## Security Considerations
|
|
117
|
+
|
|
118
|
+
This frontend is designed for demonstration purposes:
|
|
119
|
+
- No real code execution in the browser
|
|
120
|
+
- Simulated vulnerability detection
|
|
121
|
+
- Safe for educational and demo use
|
|
122
|
+
- No actual security risks
|
|
123
|
+
|
|
124
|
+
## Contributing
|
|
125
|
+
|
|
126
|
+
Feel free to enhance the terminal:
|
|
127
|
+
- Add new visualization effects
|
|
128
|
+
- Improve the command interface
|
|
129
|
+
- Add more vulnerability types
|
|
130
|
+
- Enhance the hacker aesthetic
|
|
131
|
+
|
|
132
|
+
## License
|
|
133
|
+
|
|
134
|
+
MIT License - Feel free to use and modify for your security projects.
|