opsec-tool 0.1.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,17 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 opsec contributors
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.
@@ -0,0 +1,85 @@
1
+ Metadata-Version: 2.4
2
+ Name: opsec-tool
3
+ Version: 0.1.0
4
+ Summary: Practical privacy and operational-security helpers for Python.
5
+ Author: opsec contributors
6
+ License: MIT
7
+ Requires-Python: >=3.9
8
+ Description-Content-Type: text/markdown
9
+ License-File: LICENSE
10
+ Requires-Dist: requests<3,>=2.31
11
+ Provides-Extra: dev
12
+ Requires-Dist: pytest>=8; extra == "dev"
13
+ Requires-Dist: ruff>=0.6; extra == "dev"
14
+ Dynamic: license-file
15
+
16
+ # opsec
17
+
18
+ join discord.gg/vrmodding you opsec god
19
+ we cant make u anonymous at all ts was mostly for fun. dm me "skireofficial" on discord
20
+
21
+ ## install
22
+
23
+ ```bash
24
+ python -m pip install opsec
25
+ ```
26
+
27
+ ## proxy loader
28
+
29
+ ```python
30
+ from opsec.proxy import ProxyPool
31
+
32
+ pool = ProxyPool.from_file("proxies.txt", check=True)
33
+
34
+ proxy = pool.get()
35
+ print(proxy.url)
36
+ ```
37
+
38
+ proxies.txt accepts one url per line
39
+
40
+ ```text
41
+ http://127.0.0.1:8080
42
+ socks5h://user:password@example.net:1080
43
+ ```
44
+
45
+ round robin, random, and http are all allowed
46
+
47
+ ## http session
48
+
49
+ ```python
50
+ from opsec.http import PrivacySession
51
+
52
+ session = PrivacySession(proxy_pool=pool)
53
+
54
+ response = session.get(
55
+ "https://example.com/?utm_source=newsletter&id=42",
56
+ timeout=10,
57
+ )
58
+ ```
59
+
60
+ session removes basic tracking things and retrieves it safely
61
+
62
+ ## scan
63
+
64
+ ```python
65
+ from opsec.secrets import scan_text
66
+
67
+ for finding in scan_text("token=ghp_example123"):
68
+ print(finding.kind, finding.start, finding.end)
69
+ ```
70
+ it reports matches
71
+
72
+ ## auditing
73
+
74
+ ```bash
75
+ opsec audit
76
+ ```
77
+
78
+ checks flaws and config mistakes that can affect ur privac y ## yo thingy u put a typo here fix this later when u build it im lazy and remove this
79
+
80
+ ## developemnt
81
+
82
+ ```bash
83
+ python -m pip install -e ".[dev]"
84
+ pytest
85
+ ```
@@ -0,0 +1,70 @@
1
+ # opsec
2
+
3
+ join discord.gg/vrmodding you opsec god
4
+ we cant make u anonymous at all ts was mostly for fun. dm me "skireofficial" on discord
5
+
6
+ ## install
7
+
8
+ ```bash
9
+ python -m pip install opsec
10
+ ```
11
+
12
+ ## proxy loader
13
+
14
+ ```python
15
+ from opsec.proxy import ProxyPool
16
+
17
+ pool = ProxyPool.from_file("proxies.txt", check=True)
18
+
19
+ proxy = pool.get()
20
+ print(proxy.url)
21
+ ```
22
+
23
+ proxies.txt accepts one url per line
24
+
25
+ ```text
26
+ http://127.0.0.1:8080
27
+ socks5h://user:password@example.net:1080
28
+ ```
29
+
30
+ round robin, random, and http are all allowed
31
+
32
+ ## http session
33
+
34
+ ```python
35
+ from opsec.http import PrivacySession
36
+
37
+ session = PrivacySession(proxy_pool=pool)
38
+
39
+ response = session.get(
40
+ "https://example.com/?utm_source=newsletter&id=42",
41
+ timeout=10,
42
+ )
43
+ ```
44
+
45
+ session removes basic tracking things and retrieves it safely
46
+
47
+ ## scan
48
+
49
+ ```python
50
+ from opsec.secrets import scan_text
51
+
52
+ for finding in scan_text("token=ghp_example123"):
53
+ print(finding.kind, finding.start, finding.end)
54
+ ```
55
+ it reports matches
56
+
57
+ ## auditing
58
+
59
+ ```bash
60
+ opsec audit
61
+ ```
62
+
63
+ checks flaws and config mistakes that can affect ur privac y ## yo thingy u put a typo here fix this later when u build it im lazy and remove this
64
+
65
+ ## developemnt
66
+
67
+ ```bash
68
+ python -m pip install -e ".[dev]"
69
+ pytest
70
+ ```
@@ -0,0 +1,12 @@
1
+ from .core import rid, rd, su, ae
2
+ from .proxy import Proxy, ProxyPool, ProxyStatus
3
+ from .http import PrivacySession
4
+ from .audit import AuditResult, audit
5
+
6
+ randomid = rid
7
+ redact = rd
8
+ sanitize_url = su
9
+ audit_environment = ae
10
+ run_audit = audit
11
+
12
+ __version__ = "0.1.0"
@@ -0,0 +1,5 @@
1
+ from .system import AuditResult, Finding, audit
2
+
3
+ run_audit = audit
4
+
5
+ __all__ = ["AuditResult", "Finding", "audit", "run_audit"]
@@ -0,0 +1,77 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from pathlib import Path
5
+ import os
6
+ import stat
7
+
8
+ from ..core import ae
9
+
10
+
11
+ @dataclass(frozen=True, slots=True)
12
+ class Finding:
13
+ severity: str
14
+ category: str
15
+ message: str
16
+
17
+
18
+ @dataclass(frozen=True, slots=True)
19
+ class AuditResult:
20
+ score: int
21
+ findings: list[Finding]
22
+
23
+ def as_dict(self) -> dict:
24
+ return {
25
+ "score": self.score,
26
+ "findings": [
27
+ {
28
+ "severity": item.severity,
29
+ "category": item.category,
30
+ "message": item.message,
31
+ }
32
+ for item in self.findings
33
+ ],
34
+ }
35
+
36
+
37
+ def audit() -> AuditResult:
38
+ findings: list[Finding] = []
39
+
40
+ names = ae()
41
+ if names:
42
+ findings.append(
43
+ Finding(
44
+ "high",
45
+ "environment",
46
+ f"{len(names)} environment variables look credential-related",
47
+ )
48
+ )
49
+
50
+ home = Path.home()
51
+ for path in (home / ".ssh", home / ".aws", home / ".gnupg"):
52
+ if path.exists():
53
+ mode = stat.S_IMODE(path.stat().st_mode)
54
+ if mode & 0o077:
55
+ findings.append(
56
+ Finding(
57
+ "medium",
58
+ "filesystem",
59
+ f"{path} is readable by group or other users",
60
+ )
61
+ )
62
+
63
+ if os.name == "posix":
64
+ mask = os.umask(0)
65
+ os.umask(mask)
66
+ if mask == 0:
67
+ findings.append(
68
+ Finding(
69
+ "low",
70
+ "process",
71
+ "process umask is 000; newly created files may be more permissive than intended",
72
+ )
73
+ )
74
+
75
+ weight = {"high": 30, "medium": 15, "low": 5}
76
+ score = max(0, 100 - sum(weight[item.severity] for item in findings))
77
+ return AuditResult(score, findings)
@@ -0,0 +1,76 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import json
5
+
6
+ from .audit import audit
7
+ from .core import rd, su
8
+ from .proxy import ProxyPool
9
+ from .secrets import scan_file
10
+
11
+
12
+ def main(argv: list[str] | None = None) -> int:
13
+ parser = argparse.ArgumentParser(prog="opsec")
14
+ sub = parser.add_subparsers(dest="command", required=True)
15
+
16
+ sub.add_parser("audit", help="check common local privacy mistakes")
17
+
18
+ rd_parser = sub.add_parser("rd", help="rd common credentials from text")
19
+ rd_parser.add_argument("text")
20
+
21
+ url_parser = sub.add_parser("url", help="clean a URL")
22
+ url_parser.add_argument("url")
23
+
24
+ secret_parser = sub.add_parser("secrets", help="scan one file for likely secrets")
25
+ secret_parser.add_argument("path")
26
+
27
+ proxy_parser = sub.add_parser("proxy", help="work with a proxy list")
28
+ proxy_sub = proxy_parser.add_subparsers(dest="proxy_command", required=True)
29
+ test_parser = proxy_sub.add_parser("test", help="health-check proxies")
30
+ test_parser.add_argument("path")
31
+ test_parser.add_argument("--n", type=int, default=8)
32
+
33
+ args = parser.parse_args(argv)
34
+
35
+ if args.command == "audit":
36
+ print(json.dumps(audit().as_dict(), indent=2))
37
+ return 0
38
+
39
+ if args.command == "rd":
40
+ print(rd(args.text))
41
+ return 0
42
+
43
+ if args.command == "url":
44
+ print(su(args.url))
45
+ return 0
46
+
47
+ if args.command == "secrets":
48
+ findings = scan_file(args.path)
49
+ print(json.dumps([finding.__dict__ if hasattr(finding, "__dict__") else {
50
+ "kind": finding.kind,
51
+ "start": finding.start,
52
+ "end": finding.end,
53
+ } for finding in findings], indent=2))
54
+ return 0
55
+
56
+ if args.command == "proxy" and args.proxy_command == "test":
57
+ pool = ProxyPool.from_file(args.path)
58
+ print(json.dumps(
59
+ [status.__dict__ if hasattr(status, "__dict__") else {
60
+ "url": status.url,
61
+ "healthy": status.healthy,
62
+ "lat": status.lat,
63
+ "fails": status.fails,
64
+ "oks": status.oks,
65
+ "at": status.at,
66
+ } for status in pool.check_all(n=args.n)],
67
+ indent=2,
68
+ ))
69
+ return 0
70
+
71
+ parser.error("unknown command")
72
+ return 2
73
+
74
+
75
+ if __name__ == "__main__":
76
+ raise SystemExit(main())
@@ -0,0 +1,67 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ import re
5
+ import secrets
6
+ from typing import Iterable, Mapping
7
+ from urllib.parse import parse_qsl, urlencode, urlsplit, urlunsplit
8
+
9
+ sn = re.compile(
10
+ r"(?:api[_-]?key|access[_-]?token|auth(?:orization)?|client[_-]?secret|"
11
+ r"password|passwd|private[_-]?key|secret|token)",
12
+ re.I,
13
+ )
14
+
15
+ rdors = (
16
+ re.compile(r"(?i)(authorization\s*:\s*bearer\s+)[^\s,;]+"),
17
+ re.compile(r"(?i)(authorization\s*:\s*basic\s+)[^\s,;]+"),
18
+ re.compile(r"(?i)(\b(?:password|passwd|pwd|secret|token|api[_-]?key)\s*[:=]\s*)[^\s,;]+"),
19
+ )
20
+
21
+ tk = frozenset({
22
+ "fbclid", "gclid", "dclid", "msclkid", "mc_cid", "mc_eid",
23
+ "ref", "referrer", "affiliate", "affiliate_id",
24
+ })
25
+
26
+ def rid(nbytes: int = 16) -> str:
27
+ if nbytes < 1:
28
+ raise ValueError("nbytes must be at least 1")
29
+ return secrets.token_urlsafe(nbytes)
30
+
31
+ def rd(value: str, replacement: str = "[REDACTED]") -> str:
32
+ text = str(value)
33
+ for pattern in rdors:
34
+ text = pattern.sub(lambda m: m.group(1) + replacement, text)
35
+ return text
36
+
37
+ def su(
38
+ url: str,
39
+ *,
40
+ remove_tracking: bool = True,
41
+ remove_keys: Iterable[str] = (),
42
+ ) -> str:
43
+ parts = urlsplit(url)
44
+ host = parts.hostname or ""
45
+ if ":" in host and not host.startswith("["):
46
+ host = f"[{host}]"
47
+ netloc = host
48
+ if parts.port is not None:
49
+ netloc = f"{netloc}:{parts.port}"
50
+
51
+ blocked = {key.lower() for key in remove_keys}
52
+ query = []
53
+ for key, value in parse_qsl(parts.query, keep_blank_values=True):
54
+ lower = key.lower()
55
+ if lower in blocked:
56
+ continue
57
+ if remove_tracking and (lower in tk or lower.startswith("utm_")):
58
+ continue
59
+ query.append((key, value))
60
+
61
+ return urlunsplit(
62
+ (parts.scheme, netloc, parts.path, urlencode(query, doseq=True), parts.fragment)
63
+ )
64
+
65
+ def ae(environ: Mapping[str, str] | None = None) -> list[str]:
66
+ env = os.environ if environ is None else environ
67
+ return sorted(name for name in env if sn.search(name))
@@ -0,0 +1,3 @@
1
+ from .session import PrivacySession
2
+
3
+ __all__ = ["PrivacySession"]
@@ -0,0 +1,52 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ import requests
6
+
7
+ from ..core import su
8
+ from ..proxy import ProxyPool
9
+
10
+
11
+ class PrivacySession(requests.Session):
12
+ """Requests session with conservative privacy defaults."""
13
+
14
+ def __init__(
15
+ self,
16
+ *,
17
+ proxy_pool: ProxyPool | None = None,
18
+ strip_tracking: bool = True,
19
+ ) -> None:
20
+ super().__init__()
21
+ self.proxy_pool = proxy_pool
22
+ self.strip_tracking = strip_tracking
23
+ self.headers.update(
24
+ {
25
+ "User-Agent": "opsec/0.1",
26
+ "DNT": "1",
27
+ "Accept": "*/*",
28
+ }
29
+ )
30
+
31
+ def req(self, method: str, url: str, **kwargs: Any) -> requests.Response:
32
+ if self.strip_tracking:
33
+ url = su(url)
34
+
35
+ proxy = self.proxy_pool.get() if self.proxy_pool else None
36
+ if proxy is not None:
37
+ kwargs.setdefault("proxies", proxy.requests_mapping())
38
+
39
+ try:
40
+ r = super().request(method, url, **kwargs)
41
+ except requests.RequestException:
42
+ if proxy is not None:
43
+ self.proxy_pool.bad(proxy)
44
+ raise
45
+
46
+ if proxy is not None:
47
+ if 200 <= r.status_code < 500:
48
+ self.proxy_pool.ok(proxy)
49
+ else:
50
+ self.proxy_pool.bad(proxy)
51
+
52
+ return r
@@ -0,0 +1,3 @@
1
+ from .pool import Proxy, ProxyPool, ProxyStatus
2
+
3
+ __all__ = ["Proxy", "ProxyPool", "ProxyStatus"]
@@ -0,0 +1,167 @@
1
+ from __future__ import annotations
2
+
3
+ from concurrent.futures import ThreadPoolExecutor, as_completed
4
+ from dataclasses import dataclass
5
+ import random
6
+ from pathlib import Path
7
+ import threading
8
+ import time
9
+ from typing import Iterable
10
+ from urllib.parse import urlparse
11
+
12
+ import requests
13
+
14
+
15
+ @dataclass(slots=True)
16
+ class ProxyStatus:
17
+ url: str
18
+ healthy: bool | None
19
+ lat: float | None
20
+ fails: int
21
+ oks: int
22
+ at: float | None
23
+
24
+
25
+ @dataclass(slots=True)
26
+ class Proxy:
27
+ url: str
28
+ healthy: bool | None = None
29
+ lat: float | None = None
30
+ fails: int = 0
31
+ oks: int = 0
32
+ at: float | None = None
33
+
34
+ def __post_init__(self) -> None:
35
+ parsed = urlparse(self.url)
36
+ if parsed.scheme not in {"http", "https", "socks5", "socks5h"}:
37
+ raise ValueError(f"unsupported proxy scheme: {parsed.scheme}")
38
+ if not parsed.hostname or parsed.port is None:
39
+ raise ValueError("proxy URL must include a host and port")
40
+
41
+ def requests_mapping(self) -> dict[str, str]:
42
+ return {"http": self.url, "https": self.url}
43
+
44
+ def status(self) -> ProxyStatus:
45
+ return ProxyStatus(
46
+ self.url,
47
+ self.healthy,
48
+ self.lat,
49
+ self.fails,
50
+ self.oks,
51
+ self.at,
52
+ )
53
+
54
+
55
+ class ProxyPool:
56
+ def __init__(
57
+ self,
58
+ proxies: Iterable[str | Proxy],
59
+ *,
60
+ strategy: str = "round_robin",
61
+ max_fails: int = 3,
62
+ ) -> None:
63
+ if strategy not in {"round_robin", "random", "healthy"}:
64
+ raise ValueError("strategy must be round_robin, random, or healthy")
65
+ if max_fails < 1:
66
+ raise ValueError("max_fails must be at least 1")
67
+
68
+ self.items = [p if isinstance(p, Proxy) else Proxy(p) for p in proxies]
69
+ self.strategy = strategy
70
+ self.max_fails = max_fails
71
+ self.cursor = 0
72
+ self.lock = threading.Lock()
73
+
74
+ @classmethod
75
+ def from_file(cls, path: str | Path, *, check: bool = False, **kwargs) -> "ProxyPool":
76
+ lines = Path(path).read_text(encoding="utf-8").splitlines()
77
+ urls = [
78
+ line.strip()
79
+ for line in lines
80
+ if line.strip() and not line.lstrip().startswith("#")
81
+ ]
82
+ pool = cls(urls, **kwargs)
83
+ if check:
84
+ pool.checkall()
85
+ return pool
86
+
87
+ def __len__(self) -> int:
88
+ return len(self.items)
89
+
90
+ def avail(self) -> list[Proxy]:
91
+ avail = [p for p in self.items if p.fails < self.max_fails]
92
+ return avail or list(self.items)
93
+
94
+ def get(self) -> Proxy:
95
+ with self.lock:
96
+ candidates = self.avail()
97
+ if not candidates:
98
+ raise RuntimeError("proxy pool is empty")
99
+
100
+ if self.strategy == "random":
101
+ return random.choice(candidates)
102
+
103
+ if self.strategy == "healthy":
104
+ healthy = [p for p in candidates if p.healthy is True]
105
+ return random.choice(healthy or candidates)
106
+
107
+ proxy = candidates[self.cursor % len(candidates)]
108
+ self.cursor += 1
109
+ return proxy
110
+
111
+ def check(
112
+ self,
113
+ proxy: PX,
114
+ *,
115
+ url: str = "https://www.gstatic.com/generate_204",
116
+ timeout: float = 8,
117
+ ) -> ProxyStatus:
118
+ started = time.monotonic()
119
+ try:
120
+ r = requests.get(
121
+ url,
122
+ proxies=proxy.requests_mapping(),
123
+ timeout=timeout,
124
+ allow_redirects=False,
125
+ headers={"User-Agent": "opsec/0.1"},
126
+ )
127
+ ok = 200 <= r.status_code < 400
128
+ proxy.healthy = ok
129
+ if ok:
130
+ proxy.oks += 1
131
+ else:
132
+ proxy.fails += 1
133
+ except requests.RequestException:
134
+ proxy.healthy = False
135
+ proxy.fails += 1
136
+
137
+ proxy.lat = round((time.monotonic() - started) * 1000, 1)
138
+ proxy.at = time.time()
139
+ return proxy.status()
140
+
141
+ def check_all(
142
+ self,
143
+ *,
144
+ url: str = "https://www.gstatic.com/generate_204",
145
+ timeout: float = 8,
146
+ n: int = 8,
147
+ ) -> list[ProxyStatus]:
148
+ n = max(1, min(n, len(self.items) or 1))
149
+ with ThreadPoolExecutor(max_workers=n) as executor:
150
+ futures = [
151
+ executor.submit(self.check, proxy, url=url, timeout=timeout)
152
+ for proxy in self.items
153
+ ]
154
+ return [future.result() for future in as_completed(futures)]
155
+
156
+ def record_success(self, proxy: PX) -> None:
157
+ proxy.oks += 1
158
+ proxy.fails = max(0, proxy.fails - 1)
159
+ proxy.healthy = True
160
+
161
+ def record_failure(self, proxy: PX) -> None:
162
+ proxy.fails += 1
163
+ if proxy.fails >= self.max_fails:
164
+ proxy.healthy = False
165
+
166
+ def statuses(self) -> list[ProxyStatus]:
167
+ return [proxy.status() for proxy in self.items]
@@ -0,0 +1,3 @@
1
+ from .scanner import Finding, scan_file, scan_text
2
+
3
+ __all__ = ["Finding", "scan_file", "scan_text"]
@@ -0,0 +1,39 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from pathlib import Path
5
+ import re
6
+
7
+
8
+ @dataclass(frozen=True, slots=True)
9
+ class Finding:
10
+ kind: str
11
+ start: int
12
+ end: int
13
+
14
+
15
+ pt: tuple[tuple[str, re.Pattern[str]], ...] = (
16
+ ("github_token", re.compile(r"\bgh[pousr]_[A-Za-z0-9_]{20,}\b")),
17
+ ("aws_access_key", re.compile(r"\bAKIA[0-9A-Z]{16}\b")),
18
+ ("private_key", re.compile(r"-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----")),
19
+ ("generic_assignment", re.compile(
20
+ r"(?i)\b(?:api[_-]?key|secret|token|password)\b\s*[:=]\s*['\"]?[A-Za-z0-9_\-/.+=]{8,}"
21
+ )),
22
+ )
23
+
24
+
25
+ def scan_text(text: str) -> list[Finding]:
26
+ findings: list[Finding] = []
27
+ for kind, pattern in pt:
28
+ for match in pattern.finditer(text):
29
+ findings.append(Finding(kind, match.start(), match.end()))
30
+ findings.sort(key=lambda finding: (finding.start, finding.end))
31
+ return findings
32
+
33
+
34
+ def scan_file(path: str | Path, *, max_bytes: int = 2_000_000) -> list[Finding]:
35
+ file_path = Path(path)
36
+ size = file_path.stat().st_size
37
+ if size > max_bytes:
38
+ raise ValueError(f"refusing to scan {file_path}: file is larger than {max_bytes} bytes")
39
+ return scan_text(file_path.read_text(encoding="utf-8", errors="replace"))
@@ -0,0 +1,85 @@
1
+ Metadata-Version: 2.4
2
+ Name: opsec-tool
3
+ Version: 0.1.0
4
+ Summary: Practical privacy and operational-security helpers for Python.
5
+ Author: opsec contributors
6
+ License: MIT
7
+ Requires-Python: >=3.9
8
+ Description-Content-Type: text/markdown
9
+ License-File: LICENSE
10
+ Requires-Dist: requests<3,>=2.31
11
+ Provides-Extra: dev
12
+ Requires-Dist: pytest>=8; extra == "dev"
13
+ Requires-Dist: ruff>=0.6; extra == "dev"
14
+ Dynamic: license-file
15
+
16
+ # opsec
17
+
18
+ join discord.gg/vrmodding you opsec god
19
+ we cant make u anonymous at all ts was mostly for fun. dm me "skireofficial" on discord
20
+
21
+ ## install
22
+
23
+ ```bash
24
+ python -m pip install opsec
25
+ ```
26
+
27
+ ## proxy loader
28
+
29
+ ```python
30
+ from opsec.proxy import ProxyPool
31
+
32
+ pool = ProxyPool.from_file("proxies.txt", check=True)
33
+
34
+ proxy = pool.get()
35
+ print(proxy.url)
36
+ ```
37
+
38
+ proxies.txt accepts one url per line
39
+
40
+ ```text
41
+ http://127.0.0.1:8080
42
+ socks5h://user:password@example.net:1080
43
+ ```
44
+
45
+ round robin, random, and http are all allowed
46
+
47
+ ## http session
48
+
49
+ ```python
50
+ from opsec.http import PrivacySession
51
+
52
+ session = PrivacySession(proxy_pool=pool)
53
+
54
+ response = session.get(
55
+ "https://example.com/?utm_source=newsletter&id=42",
56
+ timeout=10,
57
+ )
58
+ ```
59
+
60
+ session removes basic tracking things and retrieves it safely
61
+
62
+ ## scan
63
+
64
+ ```python
65
+ from opsec.secrets import scan_text
66
+
67
+ for finding in scan_text("token=ghp_example123"):
68
+ print(finding.kind, finding.start, finding.end)
69
+ ```
70
+ it reports matches
71
+
72
+ ## auditing
73
+
74
+ ```bash
75
+ opsec audit
76
+ ```
77
+
78
+ checks flaws and config mistakes that can affect ur privac y ## yo thingy u put a typo here fix this later when u build it im lazy and remove this
79
+
80
+ ## developemnt
81
+
82
+ ```bash
83
+ python -m pip install -e ".[dev]"
84
+ pytest
85
+ ```
@@ -0,0 +1,25 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ opsec/__init__.py
5
+ opsec/cli.py
6
+ opsec/core.py
7
+ opsec/audit/__init__.py
8
+ opsec/audit/system.py
9
+ opsec/http/__init__.py
10
+ opsec/http/session.py
11
+ opsec/proxy/__init__.py
12
+ opsec/proxy/pool.py
13
+ opsec/secrets/__init__.py
14
+ opsec/secrets/scanner.py
15
+ opsec_tool.egg-info/PKG-INFO
16
+ opsec_tool.egg-info/SOURCES.txt
17
+ opsec_tool.egg-info/dependency_links.txt
18
+ opsec_tool.egg-info/entry_points.txt
19
+ opsec_tool.egg-info/requires.txt
20
+ opsec_tool.egg-info/top_level.txt
21
+ tests/test_audit.py
22
+ tests/test_core.py
23
+ tests/test_proxy.py
24
+ tests/test_public.py
25
+ tests/test_secrets.py
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ opsec = opsec.cli:main
@@ -0,0 +1,5 @@
1
+ requests<3,>=2.31
2
+
3
+ [dev]
4
+ pytest>=8
5
+ ruff>=0.6
@@ -0,0 +1 @@
1
+ opsec
@@ -0,0 +1,25 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "opsec-tool"
7
+ version = "0.1.0"
8
+ description = "Practical privacy and operational-security helpers for Python."
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ license = {text = "MIT"}
12
+ authors = [{name = "opsec contributors"}]
13
+ dependencies = ["requests>=2.31,<3"]
14
+
15
+ [project.optional-dependencies]
16
+ dev = ["pytest>=8", "ruff>=0.6"]
17
+
18
+ [project.scripts]
19
+ opsec = "opsec.cli:main"
20
+
21
+ [tool.setuptools.packages.find]
22
+ include = ["opsec*"]
23
+
24
+ [tool.pytest.ini_options]
25
+ testpaths = ["tests"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,6 @@
1
+ from opsec.audit import audit
2
+
3
+ def test_audit_result():
4
+ result = audit()
5
+ assert 0 <= result.score <= 100
6
+ assert isinstance(result.findings, list)
@@ -0,0 +1,13 @@
1
+ from opsec.core import ae, rd, su
2
+
3
+ def test_sanitize_url_removes_userinfo_and_tracking():
4
+ result = su("https://user:password@example.com/a?utm_source=x&fbclid=y&keep=1")
5
+ assert result == "https://example.com/a?keep=1"
6
+
7
+ def test_redact():
8
+ result = rd("Authorization: Bearer abc123 password=hunter2")
9
+ assert "abc123" not in result
10
+ assert "hunter2" not in result
11
+
12
+ def test_environment_audit():
13
+ assert ae({"PATH": "/bin", "MY_API_KEY": "x", "HELLO": "world"}) == ["MY_API_KEY"]
@@ -0,0 +1,10 @@
1
+ from opsec.proxy import ProxyPool
2
+
3
+ def test_round_robin():
4
+ pool = ProxyPool(["http://127.0.0.1:8080", "http://127.0.0.2:8080"])
5
+ assert pool.get().url == "http://127.0.0.1:8080"
6
+ assert pool.get().url == "http://127.0.0.2:8080"
7
+
8
+ def test_requests_mapping():
9
+ pool = ProxyPool(["socks5h://127.0.0.1:1080"])
10
+ assert pool.get().requests_mapping()["https"].startswith("socks5h://")
@@ -0,0 +1,8 @@
1
+ import opsec
2
+ from opsec import ProxyPool, PrivacySession, sanitize_url
3
+
4
+ def test_public_api():
5
+ assert opsec.__version__ == "0.1.0"
6
+ assert ProxyPool(["http://127.0.0.1:8080"]).get().url == "http://127.0.0.1:8080"
7
+ assert sanitize_url("https://u:p@example.com/?utm_source=x&ok=1") == "https://example.com/?ok=1"
8
+ assert PrivacySession is not None
@@ -0,0 +1,5 @@
1
+ from opsec.secrets import scan_text
2
+
3
+ def test_secret_scanner():
4
+ findings = scan_text("token=ghp_abcdefghijklmnopqrstuvwxyz123456")
5
+ assert findings