Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 9 additions & 5 deletions .github/workflows/docker-publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ env:
IMAGE_NAME: ${{ github.repository }}

jobs:
build-and-push:
build-and-publish:
runs-on: ubuntu-latest
permissions:
contents: read
Expand Down Expand Up @@ -65,13 +65,15 @@ jobs:
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}}
type=raw,value=latest,enable={{is_default_branch}}
# add commit sha for unique identification
type=sha,prefix={{branch}}-
type=sha,prefix=sha-
labels: |
org.opencontainers.image.title=Naminter
org.opencontainers.image.description=The most powerful and fast username availability checker
org.opencontainers.image.vendor=3xp0rt
org.opencontainers.image.licenses=MIT
org.opencontainers.image.source=${{ github.server_url }}/${{ github.repository }}
org.opencontainers.image.revision=${{ github.sha }}
org.opencontainers.image.created=${{ steps.meta.outputs.created }}

- name: Build and push Docker image
id: build
Expand All @@ -80,14 +82,16 @@ jobs:
context: .
file: ./Dockerfile
platforms: linux/amd64,linux/arm64
push: ${{ github.event_name != 'pull_request' }}
push: ${{ github.event_name == 'release' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
provenance: true
sbom: true

- name: Generate artifact attestation
if: github.event_name != 'pull_request'
if: github.event_name == 'release'
uses: actions/attest-build-provenance@v1
with:
subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
Expand Down
5 changes: 5 additions & 0 deletions naminter/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
from .core.main import Naminter

__version__ = "1.0.5"
__author__ = "3xp0rt"
__description__ = "WhatsMyName Enumeration Tool"
__license__ = "MIT"
__email__ = "contact@3xp0rt.com"
__url__ = "https://github.com/3xp0rt/Naminter"
__all__ = ['Naminter']
10 changes: 4 additions & 6 deletions naminter/cli/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ class NaminterConfig:
filter_errors: bool = False
filter_not_found: bool = False
filter_unknown: bool = False
filter_ambiguous: bool = False

# Network and concurrency
max_tasks: int = MAX_CONCURRENT_TASKS
Expand Down Expand Up @@ -77,9 +78,7 @@ def __post_init__(self) -> None:
"using known usernames from site configurations instead."
)
if not self.self_check and not self.usernames:
error_msg = "No usernames provided and self-check not enabled."
display_error(error_msg)
raise ValueError(error_msg)
raise ValueError("No usernames provided and self-check not enabled.")
try:
if self.local_list_paths:
self.local_list_paths = [str(p) for p in self.local_list_paths]
Expand All @@ -88,9 +87,7 @@ def __post_init__(self) -> None:
if not self.local_list_paths and not self.remote_list_urls:
self.remote_list_urls = [WMN_REMOTE_URL]
except Exception as e:
error_msg = f"Configuration validation failed: {e}"
display_error(error_msg)
raise ValueError(error_msg) from e
raise ValueError(f"Configuration validation failed: {e}") from e
self.impersonate = self.get_impersonation()

def get_impersonation(self) -> Optional[str]:
Expand Down Expand Up @@ -159,5 +156,6 @@ def to_dict(self) -> Dict[str, Any]:
"filter_errors": self.filter_errors,
"filter_not_found": self.filter_not_found,
"filter_unknown": self.filter_unknown,
"filter_ambiguous": self.filter_ambiguous,
"no_progressbar": self.no_progressbar,
}
18 changes: 10 additions & 8 deletions naminter/cli/console.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from rich.tree import Tree

from ..core.models import ResultStatus, SiteResult, SelfCheckResult
from .. import __description__, __version__, __author__, __license__, __email__, __url__

console: Console = Console()

Expand All @@ -28,6 +29,7 @@
ResultStatus.UNKNOWN: "?",
ResultStatus.ERROR: "!",
ResultStatus.NOT_VALID: "X",
ResultStatus.AMBIGUOUS: "*",
}

_STATUS_STYLES: Dict[ResultStatus, Style] = {
Expand All @@ -36,6 +38,7 @@
ResultStatus.UNKNOWN: Style(color=THEME['warning']),
ResultStatus.ERROR: Style(color=THEME['error'], bold=True),
ResultStatus.NOT_VALID: Style(color=THEME['error']),
ResultStatus.AMBIGUOUS: Style(color=THEME['warning'], bold=True),
}

class ResultFormatter:
Expand Down Expand Up @@ -142,19 +145,18 @@ def _add_debug_info(self, node: Tree, response_code: Optional[int] = None, elaps
if error:
node.add(Text(f"Error: {error}", style=THEME['error']))

def display_version(version: str, author: str, description: str) -> None:
def display_version() -> None:
"""Display version and metadata of the application."""

if not all([version and version.strip(), author and author.strip(), description and description.strip()]):
raise ValueError("Version, author, and description must be non-empty strings")

version_table = Table.grid(padding=(0, 2))
version_table.add_column(style=THEME['info'])
version_table.add_column(style="bold")

version_table.add_row("Version:", version)
version_table.add_row("Author:", author)
version_table.add_row("Description:", description)
version_table.add_row("Version:", __version__)
version_table.add_row("Author:", __author__)
version_table.add_row("Description:", __description__)
version_table.add_row("License:", __license__)
version_table.add_row("Email:", __email__)
version_table.add_row("GitHub:", __url__)

panel = Panel(
version_table,
Expand Down
98 changes: 44 additions & 54 deletions naminter/cli/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,20 @@
import logging
import webbrowser
from pathlib import Path
from typing import Any, Dict, List, Optional, Tuple, Union
from typing import Annotated, Any, Dict, List, Optional, Tuple, Union

import typer
from curl_cffi import requests
from rich import box
from rich.panel import Panel
from rich.table import Table

from ..cli.config import BrowserImpersonation, NaminterConfig
from ..cli.console import (
console,
THEME,
display_error,
display_version,
display_warning,
display_version,
ResultFormatter,
)
from ..cli.exporters import Exporter
Expand All @@ -23,10 +25,7 @@
from ..core.main import Naminter
from ..core.constants import MAX_CONCURRENT_TASKS, HTTP_REQUEST_TIMEOUT_SECONDS, HTTP_ALLOW_REDIRECTS, HTTP_SSL_VERIFY, WMN_REMOTE_URL, WMN_SCHEMA_URL
from ..core.exceptions import DataError, ConfigurationError

__version__ = "1.0.5"
__author__ = "3xp0rt"
__description__ = "WhatsMyName Enumeration Tool"
from .. import __description__, __version__

app = typer.Typer(
help=__description__,
Expand Down Expand Up @@ -136,42 +135,32 @@ def _merge_data(data: Dict[str, Any]) -> None:

async def run(self) -> None:
"""Main execution method with progress tracking."""
try:
wmn_data, wmn_schema = self._load_wmn_lists(
local_list_paths=self.config.local_list_paths,
remote_list_urls=self.config.remote_list_urls,
skip_validation=self.config.skip_validation
)
wmn_data, wmn_schema = self._load_wmn_lists(
local_list_paths=self.config.local_list_paths,
remote_list_urls=self.config.remote_list_urls,
skip_validation=self.config.skip_validation
)

async with Naminter(
wmn_data=wmn_data,
wmn_schema=wmn_schema,
max_tasks=self.config.max_tasks,
timeout=self.config.timeout,
impersonate=self.config.impersonate,
verify_ssl=self.config.verify_ssl,
allow_redirects=self.config.allow_redirects,
proxy=self.config.proxy,
) as naminter:
if self.config.self_check:
results = await self._run_self_check(naminter)
else:
results = await self._run_check(naminter)

async with Naminter(
wmn_data=wmn_data,
wmn_schema=wmn_schema,
max_tasks=self.config.max_tasks,
timeout=self.config.timeout,
impersonate=self.config.impersonate,
verify_ssl=self.config.verify_ssl,
allow_redirects=self.config.allow_redirects,
proxy=self.config.proxy,
) as naminter:
if self.config.self_check:
results = await self._run_self_check(naminter)
else:
results = await self._run_check(naminter)

filtered_results = [r for r in results if self._should_include_result(r)]

if self.config.export_formats:
export_manager = Exporter(self.config.usernames or [], __version__)
export_manager.export(filtered_results, self.config.export_formats)
except KeyboardInterrupt:
display_warning("Operation interrupted")
raise typer.Exit(1)
except asyncio.TimeoutError:
display_error("Operation timed out")
raise typer.Exit(1)
except Exception as e:
display_error(f"Unexpected error: {e}")
raise typer.Exit(1)
filtered_results = [r for r in results if self._should_include_result(r)]

if self.config.export_formats:
export_manager = Exporter(self.config.usernames or [], __version__)
export_manager.export(filtered_results, self.config.export_formats)

async def _run_check(self, naminter: Naminter) -> List[SiteResult]:
"""Run the username check functionality."""
Expand Down Expand Up @@ -273,7 +262,9 @@ def _should_include_result(self, result: Union[SiteResult, SelfCheckResult]) ->
return True
elif self.config.filter_unknown and status == ResultStatus.UNKNOWN:
return True
elif not any([self.config.filter_errors, self.config.filter_not_found, self.config.filter_unknown]):
elif self.config.filter_ambiguous and status == ResultStatus.AMBIGUOUS:
return True
elif not any([self.config.filter_errors, self.config.filter_not_found, self.config.filter_unknown, self.config.filter_ambiguous]):
return status == ResultStatus.FOUND

return False
Expand Down Expand Up @@ -313,14 +304,18 @@ async def _process_result(self, result: SiteResult) -> Optional[Path]:

return response_file

@app.callback(invoke_without_command=True)
def version_callback(value: bool):
"""Callback to handle version display."""
if value:
display_version()
raise typer.Exit()

def main(
usernames: Optional[List[str]] = typer.Option(None, "--username", "-u", help="Username(s) to search for across social media platforms", show_default=False),
site_names: Optional[List[str]] = typer.Option(None, "--site", "-s", help="Specific site name(s) to check (e.g., 'GitHub', 'Twitter')", show_default=False),
version: bool = typer.Option(False, "--version", help="Display version information and exit"),
version: Annotated[Optional[bool], typer.Option("--version", help="Show version information and exit", callback=version_callback, is_eager=True)] = None,
no_color: bool = typer.Option(False, "--no-color", help="Disable colored console output"),
no_progressbar: bool = typer.Option(False, "--no-progressbar", help="Disable progress bar during execution"),
ctx: typer.Context = None,

# Input lists
local_list: Optional[List[Path]] = typer.Option(
Expand Down Expand Up @@ -388,19 +383,13 @@ def main(
filter_errors: bool = typer.Option(False, "--filter-errors", help="Show only error results in console output and exports"),
filter_not_found: bool = typer.Option(False, "--filter-not-found", help="Show only not found results in console output and exports"),
filter_unknown: bool = typer.Option(False, "--filter-unknown", help="Show only unknown results in console output and exports"),
filter_ambiguous: bool = typer.Option(False, "--filter-ambiguous", help="Show only ambiguous results in console output and exports"),
) -> None:
"""Main CLI entry point."""

if ctx and ctx.invoked_subcommand:
return

if no_color:
console.no_color = True

if version:
display_version(__version__, __author__, __description__)
raise typer.Exit()

try:
config = NaminterConfig(
usernames=usernames,
Expand Down Expand Up @@ -439,6 +428,7 @@ def main(
filter_errors=filter_errors,
filter_not_found=filter_not_found,
filter_unknown=filter_unknown,
filter_ambiguous=filter_ambiguous,
no_progressbar=no_progressbar,
)

Expand Down Expand Up @@ -472,7 +462,7 @@ def main(

def entry_point() -> None:
"""Entry point for the application."""
app()
typer.run(main)

if __name__ == "__main__":
entry_point()
3 changes: 3 additions & 0 deletions naminter/cli/progress.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ def get_progress_text(self) -> str:
unknown = self.status_counts[ResultStatus.UNKNOWN]
errors = self.status_counts[ResultStatus.ERROR]
not_valid = self.status_counts[ResultStatus.NOT_VALID]
ambiguous = self.status_counts[ResultStatus.AMBIGUOUS]

sections = [
f"[{THEME['primary']}]{rate:.1f} req/s[/]",
Expand All @@ -60,6 +61,8 @@ def get_progress_text(self) -> str:

if unknown > 0:
sections.append(f"[{THEME['warning']}]? {unknown}[/]")
if ambiguous > 0:
sections.append(f"[{THEME['warning']}]* {ambiguous}[/]")
if errors > 0:
sections.append(f"[{THEME['error']}]! {errors}[/]")
if not_valid > 0:
Expand Down
Loading