argis 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,42 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ tags: ["v*"]
7
+ pull_request:
8
+ branches: [main]
9
+
10
+ jobs:
11
+ test:
12
+ runs-on: ubuntu-latest
13
+ strategy:
14
+ matrix:
15
+ python-version: ["3.10", "3.11", "3.12"]
16
+ steps:
17
+ - uses: actions/checkout@v4
18
+ - uses: actions/setup-python@v5
19
+ with:
20
+ python-version: ${{ matrix.python-version }}
21
+ - name: Install package with dev dependencies
22
+ run: pip install -e ".[dev]"
23
+ - name: Run tests
24
+ run: pytest -v
25
+
26
+ publish:
27
+ needs: test
28
+ if: startsWith(github.ref, 'refs/tags/v')
29
+ runs-on: ubuntu-latest
30
+ steps:
31
+ - uses: actions/checkout@v4
32
+ - uses: actions/setup-python@v5
33
+ with:
34
+ python-version: "3.12"
35
+ - name: Build distribution
36
+ run: |
37
+ pip install build
38
+ python -m build
39
+ - name: Publish to PyPI
40
+ uses: pypa/gh-action-pypi-publish@release/v1
41
+ with:
42
+ password: ${{ secrets.PYPI_API_TOKEN }}
argis-0.1.0/.gitignore ADDED
@@ -0,0 +1,11 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *.egg-info/
4
+ dist/
5
+ build/
6
+ .venv/
7
+ venv/
8
+ .pytest_cache/
9
+ *.csv
10
+ *.md.export
11
+ .env
argis-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,120 @@
1
+ Metadata-Version: 2.4
2
+ Name: argis
3
+ Version: 0.1.0
4
+ Summary: Asynchronous username hunter with historical diff tracking across social platforms.
5
+ Author: mohilisop
6
+ License: MIT
7
+ Keywords: async,cli,osint,security,username
8
+ Requires-Python: >=3.10
9
+ Requires-Dist: httpx[http2]>=0.27.0
10
+ Requires-Dist: rich>=13.7.0
11
+ Requires-Dist: typer>=0.12.0
12
+ Provides-Extra: dev
13
+ Requires-Dist: pytest-asyncio>=0.23.0; extra == 'dev'
14
+ Requires-Dist: pytest>=8.0.0; extra == 'dev'
15
+ Description-Content-Type: text/markdown
16
+
17
+ # Argis 👁️
18
+
19
+ **The all-seeing username scanner.**
20
+
21
+ Argis hunts down a username across dozens of platforms concurrently, tells
22
+ you where it's registered, and — unlike most tools in this space — tracks
23
+ how that footprint *changes* over time.
24
+
25
+ Named after Argus, the hundred-eyed giant of Greek myth: one scan, every
26
+ platform, watched at once.
27
+
28
+ ## Features
29
+
30
+ - **Async everything.** Built on `httpx` + `asyncio`; scans 80+ sites in
31
+ parallel instead of one at a time.
32
+ - **Diff engine.** `--diff` compares the current scan against your last
33
+ saved run and shows exactly what got registered or deleted.
34
+ - **False-positive resistant.** Detection rules per site (status code,
35
+ page-text match, or redirect-URL match) instead of blindly trusting a
36
+ 200 OK.
37
+ - **Pretty terminal UI.** Live progress bar and color-coded results via
38
+ `rich`.
39
+ - **Exportable.** `--export csv|json|markdown` for piping into other tools.
40
+ - **Proxy / Tor support.** Route scans through a proxy or local Tor.
41
+
42
+ ## Install
43
+
44
+ ```bash
45
+ # From source, editable (for development):
46
+ pip install -e .
47
+
48
+ # Or with pipx (recommended once published):
49
+ pipx install argis
50
+ ```
51
+
52
+ Requires Python 3.10+.
53
+
54
+ ## Usage
55
+
56
+ ```bash
57
+ # Basic scan
58
+ argis scan john_doe
59
+
60
+ # Scan and compare against the last saved run
61
+ argis scan john_doe --diff
62
+
63
+ # Don't save this run to history
64
+ argis scan john_doe --no-save
65
+
66
+ # Export results
67
+ argis scan john_doe --export markdown -o john_doe_report.md
68
+
69
+ # Route through Tor
70
+ argis scan john_doe --tor
71
+
72
+ # View past scans
73
+ argis history john_doe
74
+
75
+ # Wipe saved history
76
+ argis clear-history john_doe
77
+ ```
78
+
79
+ ## How detection works
80
+
81
+ Each entry in `src/argis/sites.json` defines a URL template plus a rule for
82
+ recognizing a "not found" response:
83
+
84
+ | `error_type` | Meaning |
85
+ |----------------|-------------------------------------------------------------------|
86
+ | `status_code` | Account doesn't exist if the response status matches `error_criteria` |
87
+ | `message` | Account doesn't exist if `error_criteria` text appears in the HTML |
88
+ | `response_url` | Account doesn't exist if the final (post-redirect) URL matches |
89
+
90
+ Add your own targets by editing `sites.json` — no code changes required.
91
+
92
+ ## History storage
93
+
94
+ Scan history is stored per-username as JSON at
95
+ `~/.argis/history/<username>.json`. Each file holds a bounded list of past
96
+ snapshots (newest last), which is what `--diff` and `argis history` read
97
+ from.
98
+
99
+ ## Project layout
100
+
101
+ ```
102
+ argis/
103
+ ├── pyproject.toml
104
+ ├── src/argis/
105
+ │ ├── cli.py # typer commands
106
+ │ ├── core.py # async scanning engine
107
+ │ ├── diff.py # history storage + diff computation
108
+ │ ├── exceptions.py
109
+ │ ├── sites.json # target platforms + detection rules
110
+ │ └── utils/
111
+ │ ├── display.py # rich UI
112
+ │ ├── network.py # httpx client, UA rotation, proxy/Tor
113
+ │ └── export.py # csv/json/markdown export
114
+ └── tests/
115
+ ```
116
+
117
+ ## Disclaimer
118
+
119
+ Use responsibly. Only look up usernames you have a legitimate reason to
120
+ investigate, and respect the terms of service of the sites you query.
argis-0.1.0/README.md ADDED
@@ -0,0 +1,104 @@
1
+ # Argis 👁️
2
+
3
+ **The all-seeing username scanner.**
4
+
5
+ Argis hunts down a username across dozens of platforms concurrently, tells
6
+ you where it's registered, and — unlike most tools in this space — tracks
7
+ how that footprint *changes* over time.
8
+
9
+ Named after Argus, the hundred-eyed giant of Greek myth: one scan, every
10
+ platform, watched at once.
11
+
12
+ ## Features
13
+
14
+ - **Async everything.** Built on `httpx` + `asyncio`; scans 80+ sites in
15
+ parallel instead of one at a time.
16
+ - **Diff engine.** `--diff` compares the current scan against your last
17
+ saved run and shows exactly what got registered or deleted.
18
+ - **False-positive resistant.** Detection rules per site (status code,
19
+ page-text match, or redirect-URL match) instead of blindly trusting a
20
+ 200 OK.
21
+ - **Pretty terminal UI.** Live progress bar and color-coded results via
22
+ `rich`.
23
+ - **Exportable.** `--export csv|json|markdown` for piping into other tools.
24
+ - **Proxy / Tor support.** Route scans through a proxy or local Tor.
25
+
26
+ ## Install
27
+
28
+ ```bash
29
+ # From source, editable (for development):
30
+ pip install -e .
31
+
32
+ # Or with pipx (recommended once published):
33
+ pipx install argis
34
+ ```
35
+
36
+ Requires Python 3.10+.
37
+
38
+ ## Usage
39
+
40
+ ```bash
41
+ # Basic scan
42
+ argis scan john_doe
43
+
44
+ # Scan and compare against the last saved run
45
+ argis scan john_doe --diff
46
+
47
+ # Don't save this run to history
48
+ argis scan john_doe --no-save
49
+
50
+ # Export results
51
+ argis scan john_doe --export markdown -o john_doe_report.md
52
+
53
+ # Route through Tor
54
+ argis scan john_doe --tor
55
+
56
+ # View past scans
57
+ argis history john_doe
58
+
59
+ # Wipe saved history
60
+ argis clear-history john_doe
61
+ ```
62
+
63
+ ## How detection works
64
+
65
+ Each entry in `src/argis/sites.json` defines a URL template plus a rule for
66
+ recognizing a "not found" response:
67
+
68
+ | `error_type` | Meaning |
69
+ |----------------|-------------------------------------------------------------------|
70
+ | `status_code` | Account doesn't exist if the response status matches `error_criteria` |
71
+ | `message` | Account doesn't exist if `error_criteria` text appears in the HTML |
72
+ | `response_url` | Account doesn't exist if the final (post-redirect) URL matches |
73
+
74
+ Add your own targets by editing `sites.json` — no code changes required.
75
+
76
+ ## History storage
77
+
78
+ Scan history is stored per-username as JSON at
79
+ `~/.argis/history/<username>.json`. Each file holds a bounded list of past
80
+ snapshots (newest last), which is what `--diff` and `argis history` read
81
+ from.
82
+
83
+ ## Project layout
84
+
85
+ ```
86
+ argis/
87
+ ├── pyproject.toml
88
+ ├── src/argis/
89
+ │ ├── cli.py # typer commands
90
+ │ ├── core.py # async scanning engine
91
+ │ ├── diff.py # history storage + diff computation
92
+ │ ├── exceptions.py
93
+ │ ├── sites.json # target platforms + detection rules
94
+ │ └── utils/
95
+ │ ├── display.py # rich UI
96
+ │ ├── network.py # httpx client, UA rotation, proxy/Tor
97
+ │ └── export.py # csv/json/markdown export
98
+ └── tests/
99
+ ```
100
+
101
+ ## Disclaimer
102
+
103
+ Use responsibly. Only look up usernames you have a legitimate reason to
104
+ investigate, and respect the terms of service of the sites you query.
@@ -0,0 +1,36 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "argis"
7
+ version = "0.1.0"
8
+ description = "Asynchronous username hunter with historical diff tracking across social platforms."
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = { text = "MIT" }
12
+ authors = [{ name = "mohilisop" }]
13
+ keywords = ["osint", "username", "cli", "security", "async"]
14
+ dependencies = [
15
+ "typer>=0.12.0",
16
+ "httpx[http2]>=0.27.0",
17
+ "rich>=13.7.0",
18
+ ]
19
+
20
+ [project.optional-dependencies]
21
+ dev = [
22
+ "pytest>=8.0.0",
23
+ "pytest-asyncio>=0.23.0",
24
+ ]
25
+
26
+ [project.scripts]
27
+ argis = "argis.cli:app"
28
+
29
+ [tool.hatch.build.targets.wheel]
30
+ packages = ["src/argis"]
31
+
32
+ [tool.hatch.build.targets.wheel.force-include]
33
+ "src/argis/sites.json" = "argis/sites.json"
34
+
35
+ [tool.pytest.ini_options]
36
+ asyncio_mode = "auto"
@@ -0,0 +1,3 @@
1
+ """Argis — the all-seeing username scanner."""
2
+
3
+ __version__ = "0.1.0"
@@ -0,0 +1,4 @@
1
+ from argis.cli import app
2
+
3
+ if __name__ == "__main__":
4
+ app()
@@ -0,0 +1,147 @@
1
+ """Command-line interface for Argis. Parses flags and delegates to core.py
2
+ and diff.py — this module should stay free of scanning/HTTP logic."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import asyncio
7
+ from pathlib import Path
8
+ from typing import Optional
9
+
10
+ import typer
11
+ from rich.table import Table
12
+
13
+ from argis import diff as diffmod
14
+ from argis.core import ArgisEngine
15
+ from argis.exceptions import ArgisError
16
+ from argis.utils import display
17
+ from argis.utils.display import console
18
+ from argis.utils.export import export_results
19
+
20
+ app = typer.Typer(
21
+ help="Argis: the all-seeing username scanner. Hunt down accounts across "
22
+ "dozens of platforms and track how a username's footprint changes over time."
23
+ )
24
+
25
+
26
+ @app.command()
27
+ def scan(
28
+ username: str = typer.Argument(..., help="The target username to hunt down."),
29
+ diff: bool = typer.Option(
30
+ False, "--diff", "-d", help="Compare this scan against the last saved scan."
31
+ ),
32
+ save: bool = typer.Option(
33
+ True, "--save/--no-save", help="Save this scan to history for future --diff runs."
34
+ ),
35
+ proxy: Optional[str] = typer.Option(
36
+ None, "--proxy", help="Route requests through a proxy, e.g. socks5://127.0.0.1:9050"
37
+ ),
38
+ tor: bool = typer.Option(
39
+ False, "--tor", help="Route requests through a local Tor SOCKS5 proxy."
40
+ ),
41
+ timeout: float = typer.Option(7.0, "--timeout", help="Per-request timeout in seconds."),
42
+ concurrency: int = typer.Option(
43
+ 30, "--concurrency", help="Maximum number of simultaneous requests."
44
+ ),
45
+ export: Optional[str] = typer.Option(
46
+ None, "--export", help="Export format: csv, json, or markdown."
47
+ ),
48
+ output: Optional[Path] = typer.Option(
49
+ None, "-o", "--output", help="Output file path for --export (default: <username>.<ext>)."
50
+ ),
51
+ quiet: bool = typer.Option(
52
+ False, "--quiet", "-q", help="Suppress the progress bar and live [+] hits."
53
+ ),
54
+ ):
55
+ """Search for a target username across all configured platforms."""
56
+ if not quiet:
57
+ display.print_banner(username)
58
+
59
+ try:
60
+ engine = ArgisEngine(
61
+ username,
62
+ proxy=proxy,
63
+ use_tor=tor,
64
+ timeout=timeout,
65
+ concurrency=concurrency,
66
+ )
67
+ results = asyncio.run(engine.run_scan(quiet=quiet))
68
+ except ArgisError as exc:
69
+ console.print(f"[bold red]Error:[/bold red] {exc}")
70
+ raise typer.Exit(code=1)
71
+
72
+ if not quiet:
73
+ console.print()
74
+ display.print_results_table(results, username)
75
+ display.print_summary(results)
76
+
77
+ if diff:
78
+ previous = diffmod.get_last_scan(username)
79
+ console.print()
80
+ if previous is None:
81
+ console.print(
82
+ "[dim]No previous scan found for this username — nothing to diff "
83
+ "against. This scan will become the baseline.[/dim]"
84
+ )
85
+ else:
86
+ delta = diffmod.compute_diff(previous["results"], results)
87
+ display.print_diff(delta)
88
+
89
+ if save:
90
+ diffmod.save_scan(username, results)
91
+
92
+ if export:
93
+ fmt = export.lower()
94
+ ext = {"json": "json", "csv": "csv", "markdown": "md"}.get(fmt)
95
+ if ext is None:
96
+ console.print(f"[bold red]Unsupported export format:[/bold red] {export}")
97
+ raise typer.Exit(code=1)
98
+ out_path = output or Path(f"{username}.{ext}")
99
+ export_results(results, username, fmt, out_path)
100
+ console.print(f"[dim]Exported results to {out_path}[/dim]")
101
+
102
+
103
+ @app.command()
104
+ def history(
105
+ username: str = typer.Argument(..., help="Username whose scan history to display."),
106
+ limit: int = typer.Option(10, "--limit", help="Maximum number of past scans to show."),
107
+ ):
108
+ """Show past scan timestamps and found-profile counts for a username."""
109
+ records = diffmod.load_history(username)
110
+ if not records:
111
+ console.print(f"[dim]No history found for '{username}'.[/dim]")
112
+ return
113
+
114
+ table = Table(title=f"Scan history for @{username}")
115
+ table.add_column("#")
116
+ table.add_column("Timestamp")
117
+ table.add_column("Found")
118
+ table.add_column("Total sites")
119
+
120
+ for i, record in enumerate(records[-limit:], start=1):
121
+ found = sum(1 for r in record["results"].values() if r["status"] == "FOUND")
122
+ table.add_row(str(i), record["timestamp"], str(found), str(len(record["results"])))
123
+
124
+ console.print(table)
125
+
126
+
127
+ @app.command("clear-history")
128
+ def clear_history(
129
+ username: str = typer.Argument(..., help="Username whose history should be deleted."),
130
+ yes: bool = typer.Option(False, "--yes", "-y", help="Skip the confirmation prompt."),
131
+ ):
132
+ """Delete all saved scan history for a username."""
133
+ if not yes:
134
+ confirmed = typer.confirm(f"Delete all saved history for '{username}'?")
135
+ if not confirmed:
136
+ console.print("[dim]Cancelled.[/dim]")
137
+ raise typer.Exit()
138
+
139
+ removed = diffmod.clear_history(username)
140
+ if removed:
141
+ console.print(f"[green]History cleared for '{username}'.[/green]")
142
+ else:
143
+ console.print(f"[dim]No history existed for '{username}'.[/dim]")
144
+
145
+
146
+ if __name__ == "__main__":
147
+ app()
@@ -0,0 +1,155 @@
1
+ """The operational heart of Argis: loads site rules, runs concurrent async
2
+ HTTP checks, and applies detection rules to filter out false positives."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import asyncio
7
+ import json
8
+ import pathlib
9
+
10
+ import httpx
11
+
12
+ from argis.exceptions import SiteConfigError
13
+ from argis.utils.display import console, make_progress, print_found
14
+ from argis.utils.network import build_client, random_user_agent
15
+
16
+ # Generic markers that indicate a WAF/bot-challenge page rather than a real
17
+ # answer about account existence (e.g. Cloudflare's "Client Challenge",
18
+ # reCAPTCHA walls). These can return HTTP 200, so status-code rules alone
19
+ # would misread them as FOUND. Checked before any per-site rule runs.
20
+ _CHALLENGE_MARKERS = (
21
+ "client challenge",
22
+ "checking your browser",
23
+ "attention required",
24
+ "verify you are human",
25
+ "just a moment...",
26
+ "captcha",
27
+ )
28
+
29
+
30
+ class ArgisEngine:
31
+ def __init__(
32
+ self,
33
+ username: str,
34
+ *,
35
+ proxy: str | None = None,
36
+ use_tor: bool = False,
37
+ timeout: float = 7.0,
38
+ concurrency: int = 30,
39
+ sites_path: pathlib.Path | None = None,
40
+ ):
41
+ self.username = username
42
+ self.proxy = proxy
43
+ self.use_tor = use_tor
44
+ self.timeout = timeout
45
+ self.sites = self._load_sites(sites_path)
46
+ self._semaphore = asyncio.Semaphore(concurrency)
47
+
48
+ def _load_sites(self, sites_path: pathlib.Path | None) -> dict:
49
+ """Locate and load target site config relative to the package path."""
50
+ path = sites_path or (pathlib.Path(__file__).parent / "sites.json")
51
+ if not path.exists():
52
+ raise SiteConfigError(f"sites.json not found at {path}")
53
+ try:
54
+ with open(path, "r", encoding="utf-8") as fh:
55
+ sites = json.load(fh)
56
+ except json.JSONDecodeError as exc:
57
+ raise SiteConfigError(f"sites.json is not valid JSON: {exc}") from exc
58
+
59
+ for name, rules in sites.items():
60
+ if "url" not in rules or "error_type" not in rules:
61
+ raise SiteConfigError(
62
+ f"Site '{name}' is missing required 'url' or 'error_type' key"
63
+ )
64
+ return sites
65
+
66
+ async def check_platform(
67
+ self, client: httpx.AsyncClient, name: str, rules: dict
68
+ ) -> dict:
69
+ """Evaluate a single site against its detection rule. Never raises."""
70
+ target_url = rules["url"].format(self.username)
71
+ headers = {"User-Agent": random_user_agent()}
72
+
73
+ async with self._semaphore:
74
+ try:
75
+ response = await client.get(target_url, headers=headers)
76
+ except httpx.TooManyRedirects:
77
+ return {"status": "UNKNOWN", "url": target_url}
78
+ except httpx.TimeoutException:
79
+ return {"status": "TIMEOUT", "url": target_url}
80
+ except httpx.RequestError:
81
+ return {"status": "UNKNOWN", "url": target_url}
82
+ except Exception:
83
+ # Catches low-level transport/protocol errors that escape
84
+ # httpx's own exception hierarchy (e.g. a raw h2.ProtocolError
85
+ # from a connection torn down mid-handshake under high
86
+ # concurrency). A single misbehaving connection should never
87
+ # take down the whole scan's asyncio.gather.
88
+ return {"status": "UNKNOWN", "url": target_url}
89
+
90
+ # 429 / 403 usually mean a WAF or rate limiter intervened, not a
91
+ # legitimate answer about account existence.
92
+ if response.status_code in (403, 429):
93
+ return {"status": "BLOCKED", "url": target_url}
94
+
95
+ # Some WAFs (e.g. Cloudflare) serve a challenge page with a 200,
96
+ # which would otherwise be misread as a legitimate FOUND result.
97
+ lowered_text = response.text[:2000].lower()
98
+ if any(marker in lowered_text for marker in _CHALLENGE_MARKERS):
99
+ return {"status": "BLOCKED", "url": target_url}
100
+
101
+ error_type = rules["error_type"]
102
+ error_criteria = rules.get("error_criteria")
103
+
104
+ if error_type == "status_code":
105
+ if response.status_code == int(error_criteria):
106
+ return {"status": "NOT_FOUND", "url": target_url}
107
+ elif error_type == "message":
108
+ if error_criteria and error_criteria in response.text:
109
+ return {"status": "NOT_FOUND", "url": target_url}
110
+ elif error_type == "response_url":
111
+ if error_criteria and str(response.url).rstrip("/") == error_criteria.rstrip("/"):
112
+ return {"status": "NOT_FOUND", "url": target_url}
113
+
114
+ if response.status_code == 200:
115
+ return {"status": "FOUND", "url": target_url}
116
+
117
+ return {"status": "UNKNOWN", "url": target_url}
118
+
119
+ async def run_scan(self, *, quiet: bool = False) -> dict[str, dict]:
120
+ """Run all site checks concurrently with a live progress bar."""
121
+ results: dict[str, dict] = {}
122
+
123
+ async with build_client(
124
+ proxy=self.proxy, use_tor=self.use_tor, timeout=self.timeout
125
+ ) as client:
126
+ if quiet:
127
+ tasks = [
128
+ self.check_platform(client, name, rules)
129
+ for name, rules in self.sites.items()
130
+ ]
131
+ outcomes = await asyncio.gather(*tasks)
132
+ for (name, _), outcome in zip(self.sites.items(), outcomes):
133
+ results[name] = outcome
134
+ return results
135
+
136
+ with make_progress() as progress:
137
+ task_id = progress.add_task(
138
+ "[yellow]Probing networks...", total=len(self.sites)
139
+ )
140
+
141
+ async def run_one(name: str, rules: dict) -> None:
142
+ outcome = await self.check_platform(client, name, rules)
143
+ results[name] = outcome
144
+ if outcome["status"] == "FOUND":
145
+ progress.console.print(
146
+ f"[bold green][+][/bold green] [white]{name}:[/white] "
147
+ f"[underline cyan]{outcome['url']}[/underline cyan]"
148
+ )
149
+ progress.advance(task_id)
150
+
151
+ await asyncio.gather(
152
+ *(run_one(name, rules) for name, rules in self.sites.items())
153
+ )
154
+
155
+ return results