pathl 2.1.7__tar.gz → 2.2.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.
- pathl-2.2.0/PKG-INFO +88 -0
- pathl-2.2.0/README.md +73 -0
- pathl-2.2.0/pathl/cli.py +110 -0
- pathl-2.2.0/pathl/crypto/__init__.py +9 -0
- pathl-2.2.0/pathl/crypto/aes.py +18 -0
- pathl-2.2.0/pathl/crypto/rsa.py +16 -0
- pathl-2.2.0/pathl/crypto/utils.py +49 -0
- pathl-2.2.0/pathl/math/__init__.py +13 -0
- pathl-2.2.0/pathl/math/math.c +35 -0
- pathl-2.2.0/pathl/scaner/fastscan.c +200 -0
- pathl-2.2.0/pathl.egg-info/PKG-INFO +88 -0
- {pathl-2.1.7 → pathl-2.2.0}/pathl.egg-info/SOURCES.txt +9 -0
- pathl-2.2.0/pathl.egg-info/requires.txt +2 -0
- {pathl-2.1.7 → pathl-2.2.0}/pyproject.toml +2 -2
- pathl-2.2.0/setup.py +14 -0
- pathl-2.1.7/PKG-INFO +0 -14
- pathl-2.1.7/README.md +0 -1
- pathl-2.1.7/pathl/cli.py +0 -68
- pathl-2.1.7/pathl.egg-info/PKG-INFO +0 -14
- {pathl-2.1.7 → pathl-2.2.0}/pathl/__init__.py +0 -0
- {pathl-2.1.7 → pathl-2.2.0}/pathl/dns/dns.py +0 -0
- {pathl-2.1.7 → pathl-2.2.0}/pathl/hello/__init__.py +0 -0
- {pathl-2.1.7 → pathl-2.2.0}/pathl/hello/welcome.py +0 -0
- {pathl-2.1.7 → pathl-2.2.0}/pathl/hello/world.py +0 -0
- {pathl-2.1.7 → pathl-2.2.0}/pathl/scaner/scaner.py +0 -0
- {pathl-2.1.7 → pathl-2.2.0}/pathl.egg-info/dependency_links.txt +0 -0
- {pathl-2.1.7 → pathl-2.2.0}/pathl.egg-info/entry_points.txt +0 -0
- {pathl-2.1.7 → pathl-2.2.0}/pathl.egg-info/top_level.txt +0 -0
- {pathl-2.1.7 → pathl-2.2.0}/setup.cfg +0 -0
pathl-2.2.0/PKG-INFO
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pathl
|
|
3
|
+
Version: 2.2.0
|
|
4
|
+
Summary: General-purpose cybersecurity toolkit
|
|
5
|
+
Author: Alicja
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://pip.pathl.pl
|
|
8
|
+
Project-URL: Repository, https://github.com/AlicjaPathl/pathl
|
|
9
|
+
Project-URL: Documentation, https://pip.pathl.pl/docs
|
|
10
|
+
Project-URL: Issues, https://github.com/AlicjaPathl/pathl/issues
|
|
11
|
+
Requires-Python: >=3.8
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
Requires-Dist: rsa
|
|
14
|
+
Requires-Dist: pycryptodome
|
|
15
|
+
|
|
16
|
+
# 📦 pathl 2.1.7
|
|
17
|
+
|
|
18
|
+
**pathl** to lekki i wydajny toolkit CLI oraz biblioteka programistyczna do zadań z zakresu cyberbezpieczeństwa i kryptografii, napisana w języku Python i C.
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## 🚀 Szybki start
|
|
23
|
+
|
|
24
|
+
### Instalacja i kompilacja
|
|
25
|
+
Upewnij się, że masz zainstalowanego Pythona w wersji 3.8 lub nowszej, kompilator GCC oraz nagłówki deweloperskie Pythona (np. `python3-devel` lub `python3-dev`).
|
|
26
|
+
|
|
27
|
+
1. Zainstaluj bibliotekę wraz z zależnościami:
|
|
28
|
+
```bash
|
|
29
|
+
pip install pathl
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
2. Skompiluj natywne rozszerzenie C (`fastscan`):
|
|
33
|
+
```bash
|
|
34
|
+
make build-c
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### Użycie CLI
|
|
38
|
+
Narzędzie udostępnia prosty interfejs wiersza poleceń:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
# Szybkie skanowanie portów w C z nieblokującymi gniazdami (np. porty 1-1024 z limitem 1000 połączeń współbieżnych)
|
|
42
|
+
pathl fastscan 192.168.0.1 --start 1 --end 1024 --concurrency 1000 --timeout 0.5
|
|
43
|
+
|
|
44
|
+
# Skanowanie portów TCP w wybranym przedziale w Pythonie (starsza metoda wielowątkowa)
|
|
45
|
+
pathl scan 192.168.0.1 --start 1 --end 1024 --threads 100
|
|
46
|
+
|
|
47
|
+
# Zapytanie DNS lookup (zwraca adresy IP domeny)
|
|
48
|
+
pathl dns google.com
|
|
49
|
+
|
|
50
|
+
# Wyświetlenie powitania
|
|
51
|
+
pathl hello
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### Użycie jako biblioteka (Python API)
|
|
55
|
+
```python
|
|
56
|
+
# Test instalacji
|
|
57
|
+
from pathl.hello import World
|
|
58
|
+
World().main() # Wypisze: "Hello Its My Packeg"
|
|
59
|
+
|
|
60
|
+
# Zarządzanie kluczami i szyfrowanie z podziałem na moduły
|
|
61
|
+
from pathl.crypto import aes, rsa, utils
|
|
62
|
+
|
|
63
|
+
# 1. Generowanie i ładowanie kluczy przy użyciu klasy FileOI
|
|
64
|
+
file_io = utils.FileOI(key_file="aes.key", pv_file="rsa.priv", pub_file="rsa.pub")
|
|
65
|
+
rsa_keys = file_io.load_RSA(f=1) # Wymusza generowanie, jeśli brak kluczy
|
|
66
|
+
aes_key = file_io.load_aes(f=1)
|
|
67
|
+
|
|
68
|
+
# 2. Szyfrowanie asymetryczne (RSA)
|
|
69
|
+
rsa_cipher = rsa.CryptoIORsa(rsa_keys)
|
|
70
|
+
encrypted_rsa = rsa_cipher.crypt("Tajna wiadomosc")
|
|
71
|
+
decrypted_rsa = rsa_cipher.decrypt(encrypted_rsa)
|
|
72
|
+
|
|
73
|
+
# 3. Szyfrowanie symetryczne (AES-ECB)
|
|
74
|
+
aes_cipher = aes.CryptoIOAes(aes_key)
|
|
75
|
+
encrypted_aes = aes_cipher.crypt("Tajna wiadomosc")
|
|
76
|
+
decrypted_aes = aes_cipher.decrypt(encrypted_aes)
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
> [!NOTE]
|
|
80
|
+
> Klasy można również importować bezpośrednio z pakietu głównego:
|
|
81
|
+
> `from pathl.crypto import FileOI, CryptoIORsa, CryptoIOAes`
|
|
82
|
+
|
|
83
|
+
---
|
|
84
|
+
|
|
85
|
+
## 📚 Pełna dokumentacja
|
|
86
|
+
|
|
87
|
+
Szczegółowy opis architektury systemu, modułów API, zagadnień bezpieczeństwa oraz **analizy złożoności czasowej i pamięciowej algorytmów** znajduje się w pliku:
|
|
88
|
+
👉 **[Docs.md](file:///home/neon/pathl/Docs.md)**
|
pathl-2.2.0/README.md
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# 📦 pathl 2.1.7
|
|
2
|
+
|
|
3
|
+
**pathl** to lekki i wydajny toolkit CLI oraz biblioteka programistyczna do zadań z zakresu cyberbezpieczeństwa i kryptografii, napisana w języku Python i C.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## 🚀 Szybki start
|
|
8
|
+
|
|
9
|
+
### Instalacja i kompilacja
|
|
10
|
+
Upewnij się, że masz zainstalowanego Pythona w wersji 3.8 lub nowszej, kompilator GCC oraz nagłówki deweloperskie Pythona (np. `python3-devel` lub `python3-dev`).
|
|
11
|
+
|
|
12
|
+
1. Zainstaluj bibliotekę wraz z zależnościami:
|
|
13
|
+
```bash
|
|
14
|
+
pip install pathl
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
2. Skompiluj natywne rozszerzenie C (`fastscan`):
|
|
18
|
+
```bash
|
|
19
|
+
make build-c
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
### Użycie CLI
|
|
23
|
+
Narzędzie udostępnia prosty interfejs wiersza poleceń:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
# Szybkie skanowanie portów w C z nieblokującymi gniazdami (np. porty 1-1024 z limitem 1000 połączeń współbieżnych)
|
|
27
|
+
pathl fastscan 192.168.0.1 --start 1 --end 1024 --concurrency 1000 --timeout 0.5
|
|
28
|
+
|
|
29
|
+
# Skanowanie portów TCP w wybranym przedziale w Pythonie (starsza metoda wielowątkowa)
|
|
30
|
+
pathl scan 192.168.0.1 --start 1 --end 1024 --threads 100
|
|
31
|
+
|
|
32
|
+
# Zapytanie DNS lookup (zwraca adresy IP domeny)
|
|
33
|
+
pathl dns google.com
|
|
34
|
+
|
|
35
|
+
# Wyświetlenie powitania
|
|
36
|
+
pathl hello
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### Użycie jako biblioteka (Python API)
|
|
40
|
+
```python
|
|
41
|
+
# Test instalacji
|
|
42
|
+
from pathl.hello import World
|
|
43
|
+
World().main() # Wypisze: "Hello Its My Packeg"
|
|
44
|
+
|
|
45
|
+
# Zarządzanie kluczami i szyfrowanie z podziałem na moduły
|
|
46
|
+
from pathl.crypto import aes, rsa, utils
|
|
47
|
+
|
|
48
|
+
# 1. Generowanie i ładowanie kluczy przy użyciu klasy FileOI
|
|
49
|
+
file_io = utils.FileOI(key_file="aes.key", pv_file="rsa.priv", pub_file="rsa.pub")
|
|
50
|
+
rsa_keys = file_io.load_RSA(f=1) # Wymusza generowanie, jeśli brak kluczy
|
|
51
|
+
aes_key = file_io.load_aes(f=1)
|
|
52
|
+
|
|
53
|
+
# 2. Szyfrowanie asymetryczne (RSA)
|
|
54
|
+
rsa_cipher = rsa.CryptoIORsa(rsa_keys)
|
|
55
|
+
encrypted_rsa = rsa_cipher.crypt("Tajna wiadomosc")
|
|
56
|
+
decrypted_rsa = rsa_cipher.decrypt(encrypted_rsa)
|
|
57
|
+
|
|
58
|
+
# 3. Szyfrowanie symetryczne (AES-ECB)
|
|
59
|
+
aes_cipher = aes.CryptoIOAes(aes_key)
|
|
60
|
+
encrypted_aes = aes_cipher.crypt("Tajna wiadomosc")
|
|
61
|
+
decrypted_aes = aes_cipher.decrypt(encrypted_aes)
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
> [!NOTE]
|
|
65
|
+
> Klasy można również importować bezpośrednio z pakietu głównego:
|
|
66
|
+
> `from pathl.crypto import FileOI, CryptoIORsa, CryptoIOAes`
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
## 📚 Pełna dokumentacja
|
|
71
|
+
|
|
72
|
+
Szczegółowy opis architektury systemu, modułów API, zagadnień bezpieczeństwa oraz **analizy złożoności czasowej i pamięciowej algorytmów** znajduje się w pliku:
|
|
73
|
+
👉 **[Docs.md](file:///home/neon/pathl/Docs.md)**
|
pathl-2.2.0/pathl/cli.py
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
from pathl.scaner.scaner import run_scan
|
|
3
|
+
from pathl.dns.dns import run_dns
|
|
4
|
+
|
|
5
|
+
# reszta kodu bez zmian
|
|
6
|
+
|
|
7
|
+
def cmd_hello(args):
|
|
8
|
+
print("Hello from pathl 🔐")
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def cmd_scan(args):
|
|
12
|
+
run_scan(
|
|
13
|
+
target=args.target,
|
|
14
|
+
start=args.start,
|
|
15
|
+
end=args.end,
|
|
16
|
+
threads=args.threads,
|
|
17
|
+
timeout=args.timeout,
|
|
18
|
+
output=args.output
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def cmd_fastscan(args):
|
|
23
|
+
import sys
|
|
24
|
+
try:
|
|
25
|
+
from pathl.scaner import fastscan
|
|
26
|
+
except ImportError:
|
|
27
|
+
print("Error: fastscan C extension is not compiled. Please run 'make build-c' first.")
|
|
28
|
+
sys.exit(1)
|
|
29
|
+
|
|
30
|
+
import socket
|
|
31
|
+
print(f"[*] Target : {args.target}")
|
|
32
|
+
print(f"[*] Ports : {args.start}-{args.end}")
|
|
33
|
+
print(f"[*] Limit : {args.concurrency}")
|
|
34
|
+
print(f"[*] Timeout: {args.timeout}s\n")
|
|
35
|
+
|
|
36
|
+
timeout_ms = int(args.timeout * 1000)
|
|
37
|
+
open_ports = fastscan.scan(args.target, args.start, args.end, args.concurrency, timeout_ms)
|
|
38
|
+
|
|
39
|
+
print("\n[*] Scan finished")
|
|
40
|
+
for port in open_ports:
|
|
41
|
+
try:
|
|
42
|
+
service = socket.getservbyport(port)
|
|
43
|
+
except Exception:
|
|
44
|
+
service = "unknown"
|
|
45
|
+
print(f"[OPEN] {port}/tcp ({service})")
|
|
46
|
+
|
|
47
|
+
if args.output:
|
|
48
|
+
with open(args.output, "w") as f:
|
|
49
|
+
for port in open_ports:
|
|
50
|
+
f.write(f"{port}\n")
|
|
51
|
+
print(f"[*] Saved to {args.output}")
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def main():
|
|
55
|
+
parser = argparse.ArgumentParser(
|
|
56
|
+
prog="pathl",
|
|
57
|
+
description="General-purpose cybersecurity toolkit"
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
subparsers = parser.add_subparsers(dest="command")
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
# HELLO
|
|
64
|
+
parser_hello = subparsers.add_parser("hello")
|
|
65
|
+
parser_hello.set_defaults(func=cmd_hello)
|
|
66
|
+
|
|
67
|
+
#DNS
|
|
68
|
+
parser_dns = subparsers.add_parser("dns")
|
|
69
|
+
parser_dns.add_argument("domain")
|
|
70
|
+
parser_dns.add_argument("-p", action="store_true")
|
|
71
|
+
parser_dns.set_defaults(func=run_dns)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
# SCAN
|
|
75
|
+
parser_scan = subparsers.add_parser("scan")
|
|
76
|
+
|
|
77
|
+
parser_scan.add_argument("target")
|
|
78
|
+
|
|
79
|
+
parser_scan.add_argument("--start", type=int, default=1)
|
|
80
|
+
parser_scan.add_argument("--end", type=int, default=1024)
|
|
81
|
+
|
|
82
|
+
parser_scan.add_argument("--threads", type=int, default=100)
|
|
83
|
+
parser_scan.add_argument("--timeout", type=float, default=0.3)
|
|
84
|
+
|
|
85
|
+
parser_scan.add_argument("--output")
|
|
86
|
+
|
|
87
|
+
parser_scan.set_defaults(func=cmd_scan)
|
|
88
|
+
|
|
89
|
+
# FASTSCAN
|
|
90
|
+
parser_fastscan = subparsers.add_parser("fastscan")
|
|
91
|
+
parser_fastscan.add_argument("target")
|
|
92
|
+
parser_fastscan.add_argument("--start", type=int, default=1)
|
|
93
|
+
parser_fastscan.add_argument("--end", type=int, default=1024)
|
|
94
|
+
parser_fastscan.add_argument("--concurrency", type=int, default=1000)
|
|
95
|
+
parser_fastscan.add_argument("--timeout", type=float, default=0.5)
|
|
96
|
+
parser_fastscan.add_argument("--output")
|
|
97
|
+
parser_fastscan.set_defaults(func=cmd_fastscan)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
args = parser.parse_args()
|
|
101
|
+
|
|
102
|
+
if not args.command:
|
|
103
|
+
parser.print_help()
|
|
104
|
+
return
|
|
105
|
+
|
|
106
|
+
args.func(args)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
if __name__ == "__main__":
|
|
110
|
+
main()
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
from pathl.crypto import aes
|
|
2
|
+
from pathl.crypto import rsa
|
|
3
|
+
from pathl.crypto import utils
|
|
4
|
+
|
|
5
|
+
from pathl.crypto.utils import FileOI
|
|
6
|
+
from pathl.crypto.rsa import CryptoIORsa
|
|
7
|
+
from pathl.crypto.aes import CryptoIOAes
|
|
8
|
+
|
|
9
|
+
__all__ = ["aes", "rsa", "utils", "FileOI", "CryptoIORsa", "CryptoIOAes"]
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import hashlib
|
|
2
|
+
import secrets
|
|
3
|
+
from Crypto.Cipher import AES
|
|
4
|
+
from Crypto.Util.Padding import pad, unpad
|
|
5
|
+
|
|
6
|
+
class CryptoIOAes:
|
|
7
|
+
def __init__(self, key):
|
|
8
|
+
self.key = key
|
|
9
|
+
self.cipher = AES.new(self.key, AES.MODE_ECB)
|
|
10
|
+
|
|
11
|
+
def crypt(self, msg):
|
|
12
|
+
return self.cipher.encrypt(pad(msg.encode(), 16))
|
|
13
|
+
|
|
14
|
+
def decrypt(self, text):
|
|
15
|
+
return unpad(self.cipher.decrypt(text), 16)
|
|
16
|
+
|
|
17
|
+
def gen_key(self, lengt=256):
|
|
18
|
+
return hashlib.sha256(secrets.token_bytes(2048)).digest()
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import rsa
|
|
2
|
+
import hashlib
|
|
3
|
+
import secrets
|
|
4
|
+
|
|
5
|
+
class CryptoIORsa:
|
|
6
|
+
def __init__(self, load):
|
|
7
|
+
self.pv, self.pub = load
|
|
8
|
+
|
|
9
|
+
def crypt(self, msg):
|
|
10
|
+
return rsa.encrypt(msg.encode(), self.pub)
|
|
11
|
+
|
|
12
|
+
def decrypt(self, cipher):
|
|
13
|
+
return rsa.decrypt(cipher, self.pv)
|
|
14
|
+
|
|
15
|
+
def gen_challenge(self):
|
|
16
|
+
return hashlib.sha256(secrets.token_bytes(32)).hexdigest()
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import secrets
|
|
3
|
+
import hashlib
|
|
4
|
+
import rsa
|
|
5
|
+
|
|
6
|
+
class FileOI:
|
|
7
|
+
def __init__(self, key_file="key.key", pv_file="rsa2048", pub_file="rsa2048.pub"):
|
|
8
|
+
self.pv_file = pv_file
|
|
9
|
+
self.pub_file = pub_file
|
|
10
|
+
self.key_file = key_file
|
|
11
|
+
|
|
12
|
+
def check_file(self):
|
|
13
|
+
if not (os.path.exists(self.pv_file) and os.path.exists(self.pub_file)):
|
|
14
|
+
self.new_gen_rsa()
|
|
15
|
+
|
|
16
|
+
def new_gen_rsa(self):
|
|
17
|
+
pub, pv = rsa.newkeys(2048)
|
|
18
|
+
|
|
19
|
+
with open(self.pv_file, "wb") as w:
|
|
20
|
+
w.write(pv.save_pkcs1())
|
|
21
|
+
with open(self.pub_file, "wb") as w:
|
|
22
|
+
w.write(pub.save_pkcs1())
|
|
23
|
+
print("KEY")
|
|
24
|
+
del pub, pv
|
|
25
|
+
|
|
26
|
+
def load_RSA(self, f=0):
|
|
27
|
+
if f:
|
|
28
|
+
self.check_file()
|
|
29
|
+
with open(self.pv_file, "rb") as r:
|
|
30
|
+
pv = rsa.PrivateKey.load_pkcs1(r.read())
|
|
31
|
+
with open(self.pub_file, "rb") as r:
|
|
32
|
+
pub = rsa.PublicKey.load_pkcs1(r.read())
|
|
33
|
+
return pv, pub
|
|
34
|
+
|
|
35
|
+
def load_aes(self, f=0):
|
|
36
|
+
if f or not os.path.exists(self.key_file):
|
|
37
|
+
raw = secrets.token_bytes(32)
|
|
38
|
+
key = hashlib.sha256(raw).digest()
|
|
39
|
+
|
|
40
|
+
with open(self.key_file, "wb") as file:
|
|
41
|
+
file.write(key)
|
|
42
|
+
|
|
43
|
+
with open(self.key_file, "rb") as file:
|
|
44
|
+
key = file.read()
|
|
45
|
+
|
|
46
|
+
if len(key) != 32:
|
|
47
|
+
raise ValueError(f"Invalid key length: {len(key)}")
|
|
48
|
+
|
|
49
|
+
return key
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
|
|
3
|
+
try:
|
|
4
|
+
from pathl.math.math import add
|
|
5
|
+
except ImportError:
|
|
6
|
+
# Uprzejme powiadomienie użytkownika w razie braku kompilacji
|
|
7
|
+
def add(a, b):
|
|
8
|
+
raise ImportError(
|
|
9
|
+
"Moduł pathl.math.math (rozszerzenie C) nie został skompilowany. "
|
|
10
|
+
"Uruchom 'make build-c' w katalogu głównym projektu."
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
__all__ = ["add"]
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
#define PY_SSIZE_T_CLEAN
|
|
2
|
+
#include <Python.h>
|
|
3
|
+
|
|
4
|
+
/* Funkcja dodająca dwie liczby zmiennoprzecinkowe */
|
|
5
|
+
static PyObject* py_add(PyObject* self, PyObject* args) {
|
|
6
|
+
double a, b;
|
|
7
|
+
|
|
8
|
+
/* Parsowanie argumentów z Pythona ("dd" oznacza dwa typy double) */
|
|
9
|
+
if (!PyArg_ParseTuple(args, "dd", &a, &b)) {
|
|
10
|
+
return NULL;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/* Zwrócenie wyniku jako obiekt PyFloat */
|
|
14
|
+
return PyFloat_FromDouble(a + b);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/* Tablica metod dostępnych w module */
|
|
18
|
+
static PyMethodDef MathMethods[] = {
|
|
19
|
+
{"add", py_add, METH_VARARGS, "Dodaje dwie liczby zmiennoprzecinkowe."},
|
|
20
|
+
{NULL, NULL, 0, NULL} /* Znacznik końca tablicy */
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/* Struktura definiująca moduł */
|
|
24
|
+
static struct PyModuleDef mathmodule = {
|
|
25
|
+
PyModuleDef_HEAD_INIT,
|
|
26
|
+
"math", /* Nazwa modułu */
|
|
27
|
+
"Prosty moduł matematyczny napisany w C.", /* Dokumentacja modułu */
|
|
28
|
+
-1, /* Moduł nie przechowuje stanu w zmiennych globalnych (-1) */
|
|
29
|
+
MathMethods
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
/* Funkcja inicjalizująca moduł - wywoływana automatycznie przy imporcie */
|
|
33
|
+
PyMODINIT_FUNC PyInit_math(void) {
|
|
34
|
+
return PyModule_Create(&mathmodule);
|
|
35
|
+
}
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
#define PY_SSIZE_T_CLEAN
|
|
2
|
+
#include <Python.h>
|
|
3
|
+
#include <sys/socket.h>
|
|
4
|
+
#include <netinet/in.h>
|
|
5
|
+
#include <arpa/inet.h>
|
|
6
|
+
#include <unistd.h>
|
|
7
|
+
#include <fcntl.h>
|
|
8
|
+
#include <sys/select.h>
|
|
9
|
+
#include <sys/time.h>
|
|
10
|
+
#include <netdb.h>
|
|
11
|
+
#include <errno.h>
|
|
12
|
+
#include <string.h>
|
|
13
|
+
#include <stdlib.h>
|
|
14
|
+
|
|
15
|
+
typedef struct {
|
|
16
|
+
int fd;
|
|
17
|
+
int port;
|
|
18
|
+
struct timeval start_time;
|
|
19
|
+
int active;
|
|
20
|
+
} ActiveSocket;
|
|
21
|
+
|
|
22
|
+
static PyObject* fastscan_scan(PyObject* self, PyObject* args) {
|
|
23
|
+
char *target;
|
|
24
|
+
int start_port, end_port, concurrency, timeout_ms;
|
|
25
|
+
|
|
26
|
+
if (!PyArg_ParseTuple(args, "siiii", &target, &start_port, &end_port, &concurrency, &timeout_ms)) {
|
|
27
|
+
return NULL;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (start_port < 1 || end_port > 65535 || start_port > end_port) {
|
|
31
|
+
PyErr_SetString(PyExc_ValueError, "Invalid port range");
|
|
32
|
+
return NULL;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (concurrency < 1) {
|
|
36
|
+
PyErr_SetString(PyExc_ValueError, "Concurrency must be >= 1");
|
|
37
|
+
return NULL;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
struct hostent *he = gethostbyname(target);
|
|
41
|
+
if (!he) {
|
|
42
|
+
PyErr_SetString(PyExc_ValueError, "Could not resolve hostname");
|
|
43
|
+
return NULL;
|
|
44
|
+
}
|
|
45
|
+
struct in_addr target_ip = *(struct in_addr *)he->h_addr_list[0];
|
|
46
|
+
|
|
47
|
+
int total_ports = end_port - start_port + 1;
|
|
48
|
+
int *open_ports = malloc(sizeof(int) * total_ports);
|
|
49
|
+
if (!open_ports) {
|
|
50
|
+
return PyErr_NoMemory();
|
|
51
|
+
}
|
|
52
|
+
int open_ports_count = 0;
|
|
53
|
+
|
|
54
|
+
ActiveSocket *sockets = calloc(concurrency, sizeof(ActiveSocket));
|
|
55
|
+
if (!sockets) {
|
|
56
|
+
free(open_ports);
|
|
57
|
+
return PyErr_NoMemory();
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Release GIL for the duration of the scan
|
|
61
|
+
Py_BEGIN_ALLOW_THREADS
|
|
62
|
+
|
|
63
|
+
int next_port = start_port;
|
|
64
|
+
int active_count = 0;
|
|
65
|
+
|
|
66
|
+
while (next_port <= end_port || active_count > 0) {
|
|
67
|
+
// Fill active sockets up to concurrency
|
|
68
|
+
while (active_count < concurrency && next_port <= end_port) {
|
|
69
|
+
int port = next_port++;
|
|
70
|
+
int fd = socket(AF_INET, SOCK_STREAM, 0);
|
|
71
|
+
if (fd < 0) {
|
|
72
|
+
// Out of file descriptors? Wait for active ones to close
|
|
73
|
+
next_port--;
|
|
74
|
+
break;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Non-blocking
|
|
78
|
+
int flags = fcntl(fd, F_GETFL, 0);
|
|
79
|
+
fcntl(fd, F_SETFL, flags | O_NONBLOCK);
|
|
80
|
+
|
|
81
|
+
struct sockaddr_in addr;
|
|
82
|
+
memset(&addr, 0, sizeof(addr));
|
|
83
|
+
addr.sin_family = AF_INET;
|
|
84
|
+
addr.sin_port = htons(port);
|
|
85
|
+
addr.sin_addr = target_ip;
|
|
86
|
+
|
|
87
|
+
int res = connect(fd, (struct sockaddr *)&addr, sizeof(addr));
|
|
88
|
+
if (res == 0) {
|
|
89
|
+
open_ports[open_ports_count++] = port;
|
|
90
|
+
close(fd);
|
|
91
|
+
} else if (res < 0 && errno == EINPROGRESS) {
|
|
92
|
+
// Find empty slot
|
|
93
|
+
for (int i = 0; i < concurrency; i++) {
|
|
94
|
+
if (!sockets[i].active) {
|
|
95
|
+
sockets[i].fd = fd;
|
|
96
|
+
sockets[i].port = port;
|
|
97
|
+
gettimeofday(&sockets[i].start_time, NULL);
|
|
98
|
+
sockets[i].active = 1;
|
|
99
|
+
active_count++;
|
|
100
|
+
break;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
} else {
|
|
104
|
+
close(fd);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (active_count == 0) {
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
fd_set write_fds;
|
|
113
|
+
FD_ZERO(&write_fds);
|
|
114
|
+
int max_fd = -1;
|
|
115
|
+
for (int i = 0; i < concurrency; i++) {
|
|
116
|
+
if (sockets[i].active) {
|
|
117
|
+
FD_SET(sockets[i].fd, &write_fds);
|
|
118
|
+
if (sockets[i].fd > max_fd) {
|
|
119
|
+
max_fd = sockets[i].fd;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
struct timeval sel_tv;
|
|
125
|
+
sel_tv.tv_sec = 0;
|
|
126
|
+
sel_tv.tv_usec = 10000; // 10ms select timeout
|
|
127
|
+
|
|
128
|
+
int sel_res = select(max_fd + 1, NULL, &write_fds, NULL, &sel_tv);
|
|
129
|
+
|
|
130
|
+
struct timeval now;
|
|
131
|
+
gettimeofday(&now, NULL);
|
|
132
|
+
|
|
133
|
+
for (int i = 0; i < concurrency; i++) {
|
|
134
|
+
if (!sockets[i].active) continue;
|
|
135
|
+
|
|
136
|
+
int fd = sockets[i].fd;
|
|
137
|
+
int port = sockets[i].port;
|
|
138
|
+
int closed = 0;
|
|
139
|
+
|
|
140
|
+
if (sel_res > 0 && FD_ISSET(fd, &write_fds)) {
|
|
141
|
+
int err = 0;
|
|
142
|
+
socklen_t len = sizeof(err);
|
|
143
|
+
if (getsockopt(fd, SOL_SOCKET, SO_ERROR, &err, &len) == 0) {
|
|
144
|
+
if (err == 0) {
|
|
145
|
+
open_ports[open_ports_count++] = port;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
close(fd);
|
|
149
|
+
sockets[i].active = 0;
|
|
150
|
+
active_count--;
|
|
151
|
+
closed = 1;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (!closed) {
|
|
155
|
+
long elapsed_ms = (now.tv_sec - sockets[i].start_time.tv_sec) * 1000 +
|
|
156
|
+
(now.tv_usec - sockets[i].start_time.tv_usec) / 1000;
|
|
157
|
+
if (elapsed_ms >= timeout_ms) {
|
|
158
|
+
close(fd);
|
|
159
|
+
sockets[i].active = 0;
|
|
160
|
+
active_count--;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
free(sockets);
|
|
167
|
+
|
|
168
|
+
// Re-acquire GIL
|
|
169
|
+
Py_END_ALLOW_THREADS
|
|
170
|
+
|
|
171
|
+
PyObject *open_ports_list = PyList_New(open_ports_count);
|
|
172
|
+
if (!open_ports_list) {
|
|
173
|
+
free(open_ports);
|
|
174
|
+
return NULL;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
for (int i = 0; i < open_ports_count; i++) {
|
|
178
|
+
PyList_SetItem(open_ports_list, i, PyLong_FromLong(open_ports[i]));
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
free(open_ports);
|
|
182
|
+
return open_ports_list;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
static PyMethodDef FastscanMethods[] = {
|
|
186
|
+
{"scan", fastscan_scan, METH_VARARGS, "Scan ports using non-blocking C sockets."},
|
|
187
|
+
{NULL, NULL, 0, NULL}
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
static struct PyModuleDef fastscanmodule = {
|
|
191
|
+
PyModuleDef_HEAD_INIT,
|
|
192
|
+
"fastscan",
|
|
193
|
+
"Fast port scanner in C",
|
|
194
|
+
-1,
|
|
195
|
+
FastscanMethods
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
PyMODINIT_FUNC PyInit_fastscan(void) {
|
|
199
|
+
return PyModule_Create(&fastscanmodule);
|
|
200
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pathl
|
|
3
|
+
Version: 2.2.0
|
|
4
|
+
Summary: General-purpose cybersecurity toolkit
|
|
5
|
+
Author: Alicja
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://pip.pathl.pl
|
|
8
|
+
Project-URL: Repository, https://github.com/AlicjaPathl/pathl
|
|
9
|
+
Project-URL: Documentation, https://pip.pathl.pl/docs
|
|
10
|
+
Project-URL: Issues, https://github.com/AlicjaPathl/pathl/issues
|
|
11
|
+
Requires-Python: >=3.8
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
Requires-Dist: rsa
|
|
14
|
+
Requires-Dist: pycryptodome
|
|
15
|
+
|
|
16
|
+
# 📦 pathl 2.1.7
|
|
17
|
+
|
|
18
|
+
**pathl** to lekki i wydajny toolkit CLI oraz biblioteka programistyczna do zadań z zakresu cyberbezpieczeństwa i kryptografii, napisana w języku Python i C.
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## 🚀 Szybki start
|
|
23
|
+
|
|
24
|
+
### Instalacja i kompilacja
|
|
25
|
+
Upewnij się, że masz zainstalowanego Pythona w wersji 3.8 lub nowszej, kompilator GCC oraz nagłówki deweloperskie Pythona (np. `python3-devel` lub `python3-dev`).
|
|
26
|
+
|
|
27
|
+
1. Zainstaluj bibliotekę wraz z zależnościami:
|
|
28
|
+
```bash
|
|
29
|
+
pip install pathl
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
2. Skompiluj natywne rozszerzenie C (`fastscan`):
|
|
33
|
+
```bash
|
|
34
|
+
make build-c
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### Użycie CLI
|
|
38
|
+
Narzędzie udostępnia prosty interfejs wiersza poleceń:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
# Szybkie skanowanie portów w C z nieblokującymi gniazdami (np. porty 1-1024 z limitem 1000 połączeń współbieżnych)
|
|
42
|
+
pathl fastscan 192.168.0.1 --start 1 --end 1024 --concurrency 1000 --timeout 0.5
|
|
43
|
+
|
|
44
|
+
# Skanowanie portów TCP w wybranym przedziale w Pythonie (starsza metoda wielowątkowa)
|
|
45
|
+
pathl scan 192.168.0.1 --start 1 --end 1024 --threads 100
|
|
46
|
+
|
|
47
|
+
# Zapytanie DNS lookup (zwraca adresy IP domeny)
|
|
48
|
+
pathl dns google.com
|
|
49
|
+
|
|
50
|
+
# Wyświetlenie powitania
|
|
51
|
+
pathl hello
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### Użycie jako biblioteka (Python API)
|
|
55
|
+
```python
|
|
56
|
+
# Test instalacji
|
|
57
|
+
from pathl.hello import World
|
|
58
|
+
World().main() # Wypisze: "Hello Its My Packeg"
|
|
59
|
+
|
|
60
|
+
# Zarządzanie kluczami i szyfrowanie z podziałem na moduły
|
|
61
|
+
from pathl.crypto import aes, rsa, utils
|
|
62
|
+
|
|
63
|
+
# 1. Generowanie i ładowanie kluczy przy użyciu klasy FileOI
|
|
64
|
+
file_io = utils.FileOI(key_file="aes.key", pv_file="rsa.priv", pub_file="rsa.pub")
|
|
65
|
+
rsa_keys = file_io.load_RSA(f=1) # Wymusza generowanie, jeśli brak kluczy
|
|
66
|
+
aes_key = file_io.load_aes(f=1)
|
|
67
|
+
|
|
68
|
+
# 2. Szyfrowanie asymetryczne (RSA)
|
|
69
|
+
rsa_cipher = rsa.CryptoIORsa(rsa_keys)
|
|
70
|
+
encrypted_rsa = rsa_cipher.crypt("Tajna wiadomosc")
|
|
71
|
+
decrypted_rsa = rsa_cipher.decrypt(encrypted_rsa)
|
|
72
|
+
|
|
73
|
+
# 3. Szyfrowanie symetryczne (AES-ECB)
|
|
74
|
+
aes_cipher = aes.CryptoIOAes(aes_key)
|
|
75
|
+
encrypted_aes = aes_cipher.crypt("Tajna wiadomosc")
|
|
76
|
+
decrypted_aes = aes_cipher.decrypt(encrypted_aes)
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
> [!NOTE]
|
|
80
|
+
> Klasy można również importować bezpośrednio z pakietu głównego:
|
|
81
|
+
> `from pathl.crypto import FileOI, CryptoIORsa, CryptoIOAes`
|
|
82
|
+
|
|
83
|
+
---
|
|
84
|
+
|
|
85
|
+
## 📚 Pełna dokumentacja
|
|
86
|
+
|
|
87
|
+
Szczegółowy opis architektury systemu, modułów API, zagadnień bezpieczeństwa oraz **analizy złożoności czasowej i pamięciowej algorytmów** znajduje się w pliku:
|
|
88
|
+
👉 **[Docs.md](file:///home/neon/pathl/Docs.md)**
|
|
@@ -1,14 +1,23 @@
|
|
|
1
1
|
README.md
|
|
2
2
|
pyproject.toml
|
|
3
|
+
setup.py
|
|
3
4
|
pathl/__init__.py
|
|
4
5
|
pathl/cli.py
|
|
5
6
|
pathl.egg-info/PKG-INFO
|
|
6
7
|
pathl.egg-info/SOURCES.txt
|
|
7
8
|
pathl.egg-info/dependency_links.txt
|
|
8
9
|
pathl.egg-info/entry_points.txt
|
|
10
|
+
pathl.egg-info/requires.txt
|
|
9
11
|
pathl.egg-info/top_level.txt
|
|
12
|
+
pathl/crypto/__init__.py
|
|
13
|
+
pathl/crypto/aes.py
|
|
14
|
+
pathl/crypto/rsa.py
|
|
15
|
+
pathl/crypto/utils.py
|
|
10
16
|
pathl/dns/dns.py
|
|
11
17
|
pathl/hello/__init__.py
|
|
12
18
|
pathl/hello/welcome.py
|
|
13
19
|
pathl/hello/world.py
|
|
20
|
+
pathl/math/__init__.py
|
|
21
|
+
pathl/math/math.c
|
|
22
|
+
pathl/scaner/fastscan.c
|
|
14
23
|
pathl/scaner/scaner.py
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "pathl"
|
|
7
|
-
version = "2.
|
|
7
|
+
version = "2.2.0"
|
|
8
8
|
description = "General-purpose cybersecurity toolkit"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.8"
|
|
@@ -16,7 +16,7 @@ authors = [
|
|
|
16
16
|
# POPRAWIONE: licencja jako string, nie tabela
|
|
17
17
|
license = "MIT"
|
|
18
18
|
|
|
19
|
-
dependencies = []
|
|
19
|
+
dependencies = ["rsa","pycryptodome"]
|
|
20
20
|
|
|
21
21
|
[project.urls]
|
|
22
22
|
Homepage = "https://pip.pathl.pl"
|
pathl-2.2.0/setup.py
ADDED
pathl-2.1.7/PKG-INFO
DELETED
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: pathl
|
|
3
|
-
Version: 2.1.7
|
|
4
|
-
Summary: General-purpose cybersecurity toolkit
|
|
5
|
-
Author: Alicja
|
|
6
|
-
License-Expression: MIT
|
|
7
|
-
Project-URL: Homepage, https://pip.pathl.pl
|
|
8
|
-
Project-URL: Repository, https://github.com/AlicjaPathl/pathl
|
|
9
|
-
Project-URL: Documentation, https://pip.pathl.pl/docs
|
|
10
|
-
Project-URL: Issues, https://github.com/AlicjaPathl/pathl/issues
|
|
11
|
-
Requires-Python: >=3.8
|
|
12
|
-
Description-Content-Type: text/markdown
|
|
13
|
-
|
|
14
|
-
# pathl
|
pathl-2.1.7/README.md
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
# pathl
|
pathl-2.1.7/pathl/cli.py
DELETED
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
import argparse
|
|
2
|
-
from pathl.scaner.scaner import run_scan
|
|
3
|
-
from pathl.dns.dns import run_dns
|
|
4
|
-
|
|
5
|
-
# reszta kodu bez zmian
|
|
6
|
-
|
|
7
|
-
def cmd_hello(args):
|
|
8
|
-
print("Hello from pathl 🔐")
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
def cmd_scan(args):
|
|
12
|
-
run_scan(
|
|
13
|
-
target=args.target,
|
|
14
|
-
start=args.start,
|
|
15
|
-
end=args.end,
|
|
16
|
-
threads=args.threads,
|
|
17
|
-
timeout=args.timeout,
|
|
18
|
-
output=args.output
|
|
19
|
-
)
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
def main():
|
|
23
|
-
parser = argparse.ArgumentParser(
|
|
24
|
-
prog="pathl",
|
|
25
|
-
description="General-purpose cybersecurity toolkit"
|
|
26
|
-
)
|
|
27
|
-
|
|
28
|
-
subparsers = parser.add_subparsers(dest="command")
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
# HELLO
|
|
32
|
-
parser_hello = subparsers.add_parser("hello")
|
|
33
|
-
parser_hello.set_defaults(func=cmd_hello)
|
|
34
|
-
|
|
35
|
-
#DNS
|
|
36
|
-
parser_dns = subparsers.add_parser("dns")
|
|
37
|
-
parser_dns.add_argument("domain")
|
|
38
|
-
parser_dns.add_argument("-p", action="store_true")
|
|
39
|
-
parser_dns.set_defaults(func=run_dns)
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
# SCAN
|
|
43
|
-
parser_scan = subparsers.add_parser("scan")
|
|
44
|
-
|
|
45
|
-
parser_scan.add_argument("target")
|
|
46
|
-
|
|
47
|
-
parser_scan.add_argument("--start", type=int, default=1)
|
|
48
|
-
parser_scan.add_argument("--end", type=int, default=1024)
|
|
49
|
-
|
|
50
|
-
parser_scan.add_argument("--threads", type=int, default=100)
|
|
51
|
-
parser_scan.add_argument("--timeout", type=float, default=0.3)
|
|
52
|
-
|
|
53
|
-
parser_scan.add_argument("--output")
|
|
54
|
-
|
|
55
|
-
parser_scan.set_defaults(func=cmd_scan)
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
args = parser.parse_args()
|
|
59
|
-
|
|
60
|
-
if not args.command:
|
|
61
|
-
parser.print_help()
|
|
62
|
-
return
|
|
63
|
-
|
|
64
|
-
args.func(args)
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
if __name__ == "__main__":
|
|
68
|
-
main()
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: pathl
|
|
3
|
-
Version: 2.1.7
|
|
4
|
-
Summary: General-purpose cybersecurity toolkit
|
|
5
|
-
Author: Alicja
|
|
6
|
-
License-Expression: MIT
|
|
7
|
-
Project-URL: Homepage, https://pip.pathl.pl
|
|
8
|
-
Project-URL: Repository, https://github.com/AlicjaPathl/pathl
|
|
9
|
-
Project-URL: Documentation, https://pip.pathl.pl/docs
|
|
10
|
-
Project-URL: Issues, https://github.com/AlicjaPathl/pathl/issues
|
|
11
|
-
Requires-Python: >=3.8
|
|
12
|
-
Description-Content-Type: text/markdown
|
|
13
|
-
|
|
14
|
-
# pathl
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|