abyss-scanner 0.1.0__py3-none-any.whl
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.
- abyss/__init__.py +1 -0
- abyss/cli.py +147 -0
- abyss/core/__init__.py +0 -0
- abyss/core/installer.py +82 -0
- abyss/scanners/__init__.py +0 -0
- abyss/scanners/dependencies.py +127 -0
- abyss/scanners/secrets.py +79 -0
- abyss/ui/__init__.py +0 -0
- abyss/ui/banner.py +123 -0
- abyss/ui/menu.py +125 -0
- abyss/utils/__init__.py +0 -0
- abyss/utils/platform.py +52 -0
- abyss/utils/redact.py +25 -0
- abyss_scanner-0.1.0.dist-info/METADATA +82 -0
- abyss_scanner-0.1.0.dist-info/RECORD +17 -0
- abyss_scanner-0.1.0.dist-info/WHEEL +4 -0
- abyss_scanner-0.1.0.dist-info/entry_points.txt +2 -0
abyss/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.0"
|
abyss/cli.py
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
"""CLI principal do abyss-cli."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
import typer
|
|
7
|
+
from rich.console import Console
|
|
8
|
+
from rich.table import Table
|
|
9
|
+
|
|
10
|
+
from abyss import __version__
|
|
11
|
+
from abyss.scanners import dependencies, secrets
|
|
12
|
+
|
|
13
|
+
app = typer.Typer(
|
|
14
|
+
name="abyss",
|
|
15
|
+
help="Escaneia repositórios em busca de secrets vazados e dependências vulneráveis.",
|
|
16
|
+
no_args_is_help=False,
|
|
17
|
+
invoke_without_command=True,
|
|
18
|
+
)
|
|
19
|
+
console = Console()
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _version_callback(value: bool):
|
|
23
|
+
if value:
|
|
24
|
+
console.print(f"abyss-cli v{__version__}")
|
|
25
|
+
raise typer.Exit()
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@app.callback(invoke_without_command=True)
|
|
29
|
+
def main(
|
|
30
|
+
ctx: typer.Context,
|
|
31
|
+
version: Optional[bool] = typer.Option(
|
|
32
|
+
None, "--version", callback=_version_callback, is_eager=True,
|
|
33
|
+
help="Mostra a versão e sai.",
|
|
34
|
+
),
|
|
35
|
+
):
|
|
36
|
+
"""Sem subcomando -> abre o menu interativo."""
|
|
37
|
+
if ctx.invoked_subcommand is None:
|
|
38
|
+
from abyss.ui.menu import run_interactive
|
|
39
|
+
|
|
40
|
+
run_interactive()
|
|
41
|
+
raise typer.Exit()
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@app.command()
|
|
45
|
+
def scan(
|
|
46
|
+
path: str = typer.Argument(".", help="Caminho do repositório a escanear."),
|
|
47
|
+
secrets_only: bool = typer.Option(False, "--secrets-only", help="Roda só o scan de secrets."),
|
|
48
|
+
deps_only: bool = typer.Option(False, "--deps-only", help="Roda só o scan de dependências."),
|
|
49
|
+
json_output: Optional[Path] = typer.Option(None, "--json", help="Salva resultado em JSON no caminho informado."),
|
|
50
|
+
):
|
|
51
|
+
"""Escaneia um repositório local em busca de secrets e dependências vulneráveis."""
|
|
52
|
+
from abyss.ui.banner import show_banner
|
|
53
|
+
|
|
54
|
+
show_banner()
|
|
55
|
+
|
|
56
|
+
repo_path = str(Path(path).resolve())
|
|
57
|
+
if not Path(repo_path).exists():
|
|
58
|
+
console.print(f"[red]Erro:[/red] caminho não encontrado: {repo_path}")
|
|
59
|
+
raise typer.Exit(code=1)
|
|
60
|
+
|
|
61
|
+
console.print(f"[bold cyan]abyss[/bold cyan] escaneando [bold]{repo_path}[/bold]\n")
|
|
62
|
+
|
|
63
|
+
secret_findings = []
|
|
64
|
+
dep_findings = []
|
|
65
|
+
|
|
66
|
+
if not deps_only:
|
|
67
|
+
with console.status("[cyan]Procurando secrets vazados...[/cyan]"):
|
|
68
|
+
secret_findings = secrets.scan_repo(repo_path, progress_callback=console.print)
|
|
69
|
+
_print_secret_findings(secret_findings)
|
|
70
|
+
|
|
71
|
+
if not secrets_only:
|
|
72
|
+
with console.status("[cyan]Auditando dependências...[/cyan]"):
|
|
73
|
+
dep_findings = dependencies.scan_repo(repo_path)
|
|
74
|
+
_print_dependency_findings(dep_findings)
|
|
75
|
+
|
|
76
|
+
total = len(secret_findings) + len(dep_findings)
|
|
77
|
+
if total == 0:
|
|
78
|
+
console.print("\n[bold green]✓ Nenhum problema encontrado.[/bold green]")
|
|
79
|
+
else:
|
|
80
|
+
console.print(
|
|
81
|
+
f"\n[bold yellow]⚠ {total} problema(s) encontrado(s)[/bold yellow] "
|
|
82
|
+
f"({len(secret_findings)} secret(s), {len(dep_findings)} dependência(s) vulnerável(is))"
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
if json_output:
|
|
86
|
+
_write_json(json_output, secret_findings, dep_findings, repo_path)
|
|
87
|
+
console.print(f"[dim]Relatório salvo em {json_output}[/dim]")
|
|
88
|
+
|
|
89
|
+
if total > 0:
|
|
90
|
+
raise typer.Exit(code=1)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _print_secret_findings(findings):
|
|
94
|
+
if not findings:
|
|
95
|
+
console.print("[green]✓[/green] Nenhum secret encontrado.\n")
|
|
96
|
+
return
|
|
97
|
+
|
|
98
|
+
table = Table(title="Secrets encontrados")
|
|
99
|
+
table.add_column("Arquivo", style="cyan")
|
|
100
|
+
table.add_column("Linha")
|
|
101
|
+
table.add_column("Regra", style="yellow")
|
|
102
|
+
table.add_column("Secret (redigido)", style="red")
|
|
103
|
+
|
|
104
|
+
for f in findings:
|
|
105
|
+
table.add_row(f.file, str(f.line_number), f.rule_id, f.redacted_secret)
|
|
106
|
+
|
|
107
|
+
console.print(table)
|
|
108
|
+
console.print()
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _print_dependency_findings(findings):
|
|
112
|
+
if not findings:
|
|
113
|
+
console.print("[green]✓[/green] Nenhuma dependência vulnerável encontrada.\n")
|
|
114
|
+
return
|
|
115
|
+
|
|
116
|
+
table = Table(title="Dependências vulneráveis")
|
|
117
|
+
table.add_column("Pacote", style="cyan")
|
|
118
|
+
table.add_column("Versão instalada")
|
|
119
|
+
table.add_column("Vulnerabilidade", style="yellow")
|
|
120
|
+
table.add_column("Severidade")
|
|
121
|
+
table.add_column("Correção", style="green")
|
|
122
|
+
table.add_column("Ecossistema")
|
|
123
|
+
|
|
124
|
+
for f in findings:
|
|
125
|
+
table.add_row(
|
|
126
|
+
f.package, f.installed_version, f.vulnerability_id,
|
|
127
|
+
f.severity, f.fix_version or "-", f.ecosystem,
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
console.print(table)
|
|
131
|
+
console.print()
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _write_json(output_path: Path, secret_findings, dep_findings, repo_path: str):
|
|
135
|
+
import json as json_lib
|
|
136
|
+
from dataclasses import asdict
|
|
137
|
+
|
|
138
|
+
report = {
|
|
139
|
+
"repo": repo_path,
|
|
140
|
+
"secrets": [asdict(f) for f in secret_findings],
|
|
141
|
+
"dependencies": [asdict(f) for f in dep_findings],
|
|
142
|
+
}
|
|
143
|
+
output_path.write_text(json_lib.dumps(report, indent=2, ensure_ascii=False), encoding="utf-8")
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
if __name__ == "__main__":
|
|
147
|
+
app()
|
abyss/core/__init__.py
ADDED
|
File without changes
|
abyss/core/installer.py
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"""Baixa e gerencia o binário do gitleaks para a plataforma atual.
|
|
2
|
+
|
|
3
|
+
O gitleaks é distribuído como binário standalone por release no GitHub,
|
|
4
|
+
então cada SO/arquitetura tem um asset diferente. Baixamos uma vez e
|
|
5
|
+
guardamos em abyss_home() pra não repetir o download a cada scan.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import stat
|
|
9
|
+
import tarfile
|
|
10
|
+
import zipfile
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
import requests
|
|
14
|
+
|
|
15
|
+
from abyss.utils.platform import binary_name, get_arch, get_os, abyss_home
|
|
16
|
+
|
|
17
|
+
GITLEAKS_VERSION = "8.18.4"
|
|
18
|
+
GITLEAKS_REPO = "gitleaks/gitleaks"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _asset_name() -> str:
|
|
22
|
+
"""Monta o nome do asset de release do gitleaks pra plataforma atual."""
|
|
23
|
+
os_name = get_os()
|
|
24
|
+
arch = get_arch()
|
|
25
|
+
|
|
26
|
+
os_map = {"windows": "windows", "darwin": "darwin", "linux": "linux"}
|
|
27
|
+
arch_map = {"x64": "x64", "arm64": "arm64", "x32": "x32"}
|
|
28
|
+
|
|
29
|
+
ext = "zip" if os_name == "windows" else "tar.gz"
|
|
30
|
+
return f"gitleaks_{GITLEAKS_VERSION}_{os_map[os_name]}_{arch_map.get(arch, 'x64')}.{ext}"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def gitleaks_path() -> Path:
|
|
34
|
+
return abyss_home() / binary_name("gitleaks")
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def is_installed() -> bool:
|
|
38
|
+
return gitleaks_path().exists()
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def install(progress_callback=None) -> Path:
|
|
42
|
+
"""Baixa e extrai o binário do gitleaks se ainda não estiver instalado."""
|
|
43
|
+
target = gitleaks_path()
|
|
44
|
+
if target.exists():
|
|
45
|
+
return target
|
|
46
|
+
|
|
47
|
+
asset = _asset_name()
|
|
48
|
+
url = f"https://github.com/{GITLEAKS_REPO}/releases/download/v{GITLEAKS_VERSION}/{asset}"
|
|
49
|
+
|
|
50
|
+
download_dir = abyss_home() / "downloads"
|
|
51
|
+
download_dir.mkdir(exist_ok=True)
|
|
52
|
+
archive_path = download_dir / asset
|
|
53
|
+
|
|
54
|
+
if progress_callback:
|
|
55
|
+
progress_callback(f"Baixando gitleaks v{GITLEAKS_VERSION} para {get_os()}/{get_arch()}...")
|
|
56
|
+
|
|
57
|
+
response = requests.get(url, stream=True, timeout=60)
|
|
58
|
+
response.raise_for_status()
|
|
59
|
+
|
|
60
|
+
with open(archive_path, "wb") as f:
|
|
61
|
+
for chunk in response.iter_content(chunk_size=8192):
|
|
62
|
+
f.write(chunk)
|
|
63
|
+
|
|
64
|
+
if progress_callback:
|
|
65
|
+
progress_callback("Extraindo binário...")
|
|
66
|
+
|
|
67
|
+
if asset.endswith(".zip"):
|
|
68
|
+
with zipfile.ZipFile(archive_path, "r") as zf:
|
|
69
|
+
zf.extractall(download_dir)
|
|
70
|
+
else:
|
|
71
|
+
with tarfile.open(archive_path, "r:gz") as tf:
|
|
72
|
+
tf.extractall(download_dir)
|
|
73
|
+
|
|
74
|
+
extracted_binary = download_dir / binary_name("gitleaks")
|
|
75
|
+
extracted_binary.replace(target)
|
|
76
|
+
|
|
77
|
+
if get_os() != "windows":
|
|
78
|
+
target.chmod(target.stat().st_mode | stat.S_IEXEC | stat.S_IXGRP | stat.S_IXOTH)
|
|
79
|
+
|
|
80
|
+
archive_path.unlink(missing_ok=True)
|
|
81
|
+
|
|
82
|
+
return target
|
|
File without changes
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"""Scanner de dependências vulneráveis: Python (pip-audit) e Node (npm audit).
|
|
2
|
+
|
|
3
|
+
Detecta automaticamente qual ecossistema está presente no repositório
|
|
4
|
+
(requirements.txt/pyproject.toml para Python, package.json para Node)
|
|
5
|
+
e roda a ferramenta apropriada.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
import shutil
|
|
10
|
+
import subprocess
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import List
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass
|
|
17
|
+
class DependencyFinding:
|
|
18
|
+
package: str
|
|
19
|
+
installed_version: str
|
|
20
|
+
vulnerability_id: str
|
|
21
|
+
severity: str
|
|
22
|
+
fix_version: str = ""
|
|
23
|
+
ecosystem: str = "python"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _tool_available(name: str) -> bool:
|
|
27
|
+
return shutil.which(name) is not None
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def scan_python(repo_path: str) -> List[DependencyFinding]:
|
|
31
|
+
"""Roda pip-audit se houver requirements.txt ou pyproject.toml."""
|
|
32
|
+
repo = Path(repo_path)
|
|
33
|
+
requirements_file = repo / "requirements.txt"
|
|
34
|
+
pyproject_file = repo / "pyproject.toml"
|
|
35
|
+
|
|
36
|
+
if not requirements_file.exists() and not pyproject_file.exists():
|
|
37
|
+
return []
|
|
38
|
+
|
|
39
|
+
if not _tool_available("pip-audit"):
|
|
40
|
+
return []
|
|
41
|
+
|
|
42
|
+
# requirements.txt precisa da flag -r; pyproject.toml é lido via
|
|
43
|
+
# project_path posicional (pip-audit entende PEP 621 automaticamente).
|
|
44
|
+
if requirements_file.exists():
|
|
45
|
+
command = ["pip-audit", "-r", str(requirements_file), "--format", "json"]
|
|
46
|
+
else:
|
|
47
|
+
command = ["pip-audit", str(repo), "--format", "json"]
|
|
48
|
+
|
|
49
|
+
result = subprocess.run(
|
|
50
|
+
command,
|
|
51
|
+
capture_output=True,
|
|
52
|
+
text=True,
|
|
53
|
+
timeout=180,
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
findings = []
|
|
57
|
+
try:
|
|
58
|
+
data = json.loads(result.stdout) if result.stdout.strip() else {"dependencies": []}
|
|
59
|
+
except json.JSONDecodeError:
|
|
60
|
+
return []
|
|
61
|
+
|
|
62
|
+
for dep in data.get("dependencies", []):
|
|
63
|
+
for vuln in dep.get("vulns", []):
|
|
64
|
+
findings.append(
|
|
65
|
+
DependencyFinding(
|
|
66
|
+
package=dep.get("name", "unknown"),
|
|
67
|
+
installed_version=dep.get("version", ""),
|
|
68
|
+
vulnerability_id=vuln.get("id", ""),
|
|
69
|
+
severity=vuln.get("severity", "unknown") or "unknown",
|
|
70
|
+
fix_version=", ".join(vuln.get("fix_versions", [])),
|
|
71
|
+
ecosystem="python",
|
|
72
|
+
)
|
|
73
|
+
)
|
|
74
|
+
return findings
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def scan_node(repo_path: str) -> List[DependencyFinding]:
|
|
78
|
+
"""Roda npm audit se houver package.json."""
|
|
79
|
+
repo = Path(repo_path)
|
|
80
|
+
if not (repo / "package.json").exists():
|
|
81
|
+
return []
|
|
82
|
+
|
|
83
|
+
if not _tool_available("npm"):
|
|
84
|
+
return []
|
|
85
|
+
|
|
86
|
+
result = subprocess.run(
|
|
87
|
+
["npm", "audit", "--json"],
|
|
88
|
+
capture_output=True,
|
|
89
|
+
text=True,
|
|
90
|
+
timeout=180,
|
|
91
|
+
cwd=repo_path,
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
findings = []
|
|
95
|
+
try:
|
|
96
|
+
data = json.loads(result.stdout) if result.stdout.strip() else {}
|
|
97
|
+
except json.JSONDecodeError:
|
|
98
|
+
return []
|
|
99
|
+
|
|
100
|
+
vulnerabilities = data.get("vulnerabilities", {})
|
|
101
|
+
for pkg_name, vuln_info in vulnerabilities.items():
|
|
102
|
+
via = vuln_info.get("via", [])
|
|
103
|
+
vuln_id = ""
|
|
104
|
+
for v in via:
|
|
105
|
+
if isinstance(v, dict):
|
|
106
|
+
vuln_id = str(v.get("source", ""))
|
|
107
|
+
break
|
|
108
|
+
|
|
109
|
+
findings.append(
|
|
110
|
+
DependencyFinding(
|
|
111
|
+
package=pkg_name,
|
|
112
|
+
installed_version=vuln_info.get("range", ""),
|
|
113
|
+
vulnerability_id=vuln_id or "n/a",
|
|
114
|
+
severity=vuln_info.get("severity", "unknown"),
|
|
115
|
+
fix_version=str(vuln_info.get("fixAvailable", "")),
|
|
116
|
+
ecosystem="node",
|
|
117
|
+
)
|
|
118
|
+
)
|
|
119
|
+
return findings
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def scan_repo(repo_path: str) -> List[DependencyFinding]:
|
|
123
|
+
"""Roda todos os scanners de dependência aplicáveis ao repositório."""
|
|
124
|
+
findings = []
|
|
125
|
+
findings.extend(scan_python(repo_path))
|
|
126
|
+
findings.extend(scan_node(repo_path))
|
|
127
|
+
return findings
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"""Scanner de secrets vazados, usando gitleaks como engine de detecção."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import subprocess
|
|
5
|
+
import tempfile
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import List
|
|
9
|
+
|
|
10
|
+
from abyss.core.installer import gitleaks_path, install, is_installed
|
|
11
|
+
from abyss.utils.redact import redact
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass
|
|
15
|
+
class SecretFinding:
|
|
16
|
+
rule_id: str
|
|
17
|
+
file: str
|
|
18
|
+
line_number: int
|
|
19
|
+
redacted_secret: str
|
|
20
|
+
commit: str = ""
|
|
21
|
+
severity: str = "high"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def ensure_gitleaks(progress_callback=None) -> Path:
|
|
25
|
+
if not is_installed():
|
|
26
|
+
return install(progress_callback=progress_callback)
|
|
27
|
+
return gitleaks_path()
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def scan_repo(repo_path: str, progress_callback=None) -> List[SecretFinding]:
|
|
31
|
+
"""Roda gitleaks contra um diretório local e retorna findings já
|
|
32
|
+
redigidos (o secret cru nunca fica em memória além dessa função)."""
|
|
33
|
+
|
|
34
|
+
binary = ensure_gitleaks(progress_callback=progress_callback)
|
|
35
|
+
is_git_repo = (Path(repo_path) / ".git").exists()
|
|
36
|
+
|
|
37
|
+
with tempfile.NamedTemporaryFile(suffix=".json", delete=False) as tmp:
|
|
38
|
+
report_path = tmp.name
|
|
39
|
+
|
|
40
|
+
try:
|
|
41
|
+
command = [
|
|
42
|
+
str(binary),
|
|
43
|
+
"detect",
|
|
44
|
+
"--source", repo_path,
|
|
45
|
+
"--report-format", "json",
|
|
46
|
+
"--report-path", report_path,
|
|
47
|
+
"--exit-code", "0",
|
|
48
|
+
"--no-banner",
|
|
49
|
+
]
|
|
50
|
+
if not is_git_repo:
|
|
51
|
+
command.append("--no-git")
|
|
52
|
+
|
|
53
|
+
result = subprocess.run(
|
|
54
|
+
command,
|
|
55
|
+
capture_output=True,
|
|
56
|
+
text=True,
|
|
57
|
+
timeout=300,
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
report_file = Path(report_path)
|
|
61
|
+
content = report_file.read_text(encoding="utf-8").strip()
|
|
62
|
+
raw_findings = json.loads(content) if content else []
|
|
63
|
+
|
|
64
|
+
findings = []
|
|
65
|
+
for item in raw_findings:
|
|
66
|
+
raw_secret = item.get("Secret", "")
|
|
67
|
+
findings.append(
|
|
68
|
+
SecretFinding(
|
|
69
|
+
rule_id=item.get("RuleID", "unknown"),
|
|
70
|
+
file=item.get("File", ""),
|
|
71
|
+
line_number=item.get("StartLine", 0),
|
|
72
|
+
redacted_secret=redact(raw_secret),
|
|
73
|
+
commit=item.get("Commit", "")[:8],
|
|
74
|
+
)
|
|
75
|
+
)
|
|
76
|
+
return findings
|
|
77
|
+
|
|
78
|
+
finally:
|
|
79
|
+
Path(report_path).unlink(missing_ok=True)
|
abyss/ui/__init__.py
ADDED
|
File without changes
|
abyss/ui/banner.py
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
"""Banner ASCII e identidade visual do abyss-cli.
|
|
2
|
+
Layout inspirado em splash screens clássicas de ferramenta de terminal:
|
|
3
|
+
título no canto esquerdo, arte original ao lado, seguido da tagline,
|
|
4
|
+
linha de status e prompt de "iniciando" antes de cair no menu.
|
|
5
|
+
"""
|
|
6
|
+
from rich.console import Console
|
|
7
|
+
from rich.text import Text
|
|
8
|
+
|
|
9
|
+
console = Console()
|
|
10
|
+
|
|
11
|
+
MASCOT_ART = r"""
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
--
|
|
16
|
+
=+--=+# =++-
|
|
17
|
+
-*- --+ +* =-+--
|
|
18
|
+
- =+- --= -==--=+= =+*+==* -++
|
|
19
|
+
=+- ++=+- --=--=+---*= --+*=--== =++
|
|
20
|
+
-== =+++ ---=--===----------------=-=++- ++* =--==
|
|
21
|
+
--=- -=*-------= -=+=-----------------++==---=-=+= -----=
|
|
22
|
+
=--=----------------=---=+=------------= -=++=----------------= -=
|
|
23
|
+
=-----------=----=--=---==-----------=--=+==-------------------++
|
|
24
|
+
----------------=----==---------=--===----------==---==
|
|
25
|
+
-------------- ---===----------==------------=+ --
|
|
26
|
+
-- ----------------------------==---=--=-==+
|
|
27
|
+
-=-----==-=-- --- ----------==-----=*-
|
|
28
|
+
--=-- - ----- ---------------------=+*
|
|
29
|
+
=**+=--= ----------------- =*#%
|
|
30
|
+
@█**+-==------------------------==--+%@█ -
|
|
31
|
+
+*+++--------------------------+++**
|
|
32
|
+
-=------------=--------------=+*# -
|
|
33
|
+
==------=------=-===--==----=+*
|
|
34
|
+
- --------------=-=----=--=====-===**----
|
|
35
|
+
--------------------==-===-=======-------------------
|
|
36
|
+
-==-----------------=---------=+--=-==-----------=-
|
|
37
|
+
---=====-+%**-==---=--=---=#%#+====--=====-
|
|
38
|
+
=--=%%=-=---+%*#*#*+
|
|
39
|
+
-++++++++***+= -
|
|
40
|
+
-====-- -
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
TITLE_BLOCK = r"""
|
|
44
|
+
▄▄▄ ▄▄▄▄ ▓██ ██▓ ██████ ██████
|
|
45
|
+
▒████▄ ▓█████▄▒██ ██▒▒██ ▒ ▒██ ▒
|
|
46
|
+
▒██ ▀█▄ ▒██▒ ▄██▒██ ██░░ ▓██▄ ░ ▓██▄
|
|
47
|
+
░██▄▄▄▄██ ▒██░█▀ ░ ▐██▓░ ▒ ██▒ ▒ ██▒
|
|
48
|
+
▓█ ▓██▒░▓█ ▀█▓░ ██▒▓░▒██████▒▒▒██████▒▒
|
|
49
|
+
▒▒ ▓▒█░░▒▓███▀▒ ██▒▒▒ ▒ ▒▓▒ ▒ ░▒ ▒▓▒ ▒ ░
|
|
50
|
+
▒ ▒▒ ░▒░▒ ░▓██ ░▒░ ░ ░▒ ░ ░░ ░▒ ░ ░
|
|
51
|
+
░ ▒ ░ ░▒ ▒ ░░ ░ ░ ░ ░ ░ ░
|
|
52
|
+
░ ░ ░ ░ ░ ░ ░
|
|
53
|
+
░░ ░
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
TITLE = "ABYSS"
|
|
57
|
+
SUBTITLE = "Next-Gen Deep Sea Exploration Platform"
|
|
58
|
+
STRAPLINE = "ABYSS Security — Just one bite"
|
|
59
|
+
STATUS_LINE = "[ STATUS: ACTIVE ]"
|
|
60
|
+
START_LINE = "Descendo ao abismo..."
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _combine_side_by_side(
|
|
64
|
+
left: str,
|
|
65
|
+
right: str,
|
|
66
|
+
gap: int = 3,
|
|
67
|
+
left_style: str = "bold red",
|
|
68
|
+
right_style: str = "bold magenta",
|
|
69
|
+
valign: str = "center",
|
|
70
|
+
) -> Text:
|
|
71
|
+
"""Junta dois blocos ASCII lado a lado, alinhados à esquerda (sem centralizar)."""
|
|
72
|
+
left_lines = left.strip("\n").split("\n")
|
|
73
|
+
right_lines = right.strip("\n").split("\n")
|
|
74
|
+
|
|
75
|
+
left_width = max(len(l) for l in left_lines)
|
|
76
|
+
height = max(len(left_lines), len(right_lines))
|
|
77
|
+
|
|
78
|
+
def pad_block(lines, target_height):
|
|
79
|
+
missing = target_height - len(lines)
|
|
80
|
+
if missing <= 0:
|
|
81
|
+
return lines
|
|
82
|
+
if valign == "center":
|
|
83
|
+
top = missing // 2
|
|
84
|
+
bottom = missing - top
|
|
85
|
+
elif valign == "bottom":
|
|
86
|
+
top, bottom = missing, 0
|
|
87
|
+
else: # "top"
|
|
88
|
+
top, bottom = 0, missing
|
|
89
|
+
return [""] * top + lines + [""] * bottom
|
|
90
|
+
|
|
91
|
+
left_lines = pad_block(left_lines, height)
|
|
92
|
+
right_lines = pad_block(right_lines, height)
|
|
93
|
+
|
|
94
|
+
text = Text()
|
|
95
|
+
for l, r in zip(left_lines, right_lines):
|
|
96
|
+
text.append(l.ljust(left_width), style=left_style)
|
|
97
|
+
text.append(" " * gap)
|
|
98
|
+
text.append(r, style=right_style)
|
|
99
|
+
text.append("\n")
|
|
100
|
+
return text
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def show_banner():
|
|
104
|
+
combined = _combine_side_by_side(
|
|
105
|
+
TITLE_BLOCK,
|
|
106
|
+
MASCOT_ART,
|
|
107
|
+
gap=3,
|
|
108
|
+
left_style="bold red",
|
|
109
|
+
right_style="bold magenta",
|
|
110
|
+
valign="center",
|
|
111
|
+
)
|
|
112
|
+
console.print(combined, soft_wrap=True)
|
|
113
|
+
console.print(Text(f" {SUBTITLE}", style="bold yellow"))
|
|
114
|
+
console.print(Text(f" {STRAPLINE}", style="bold green"))
|
|
115
|
+
console.print(Text(f" {STATUS_LINE}", style="bold red"))
|
|
116
|
+
console.print(Text(f" {START_LINE}", style="bold magenta"))
|
|
117
|
+
console.print()
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def section(title: str):
|
|
121
|
+
console.print()
|
|
122
|
+
console.rule(f"[bold cyan] {title} [/bold cyan]", style="cyan")
|
|
123
|
+
console.print()
|
abyss/ui/menu.py
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
"""Menu interativo estilo ferramenta OSINT — navegação por setas, sem
|
|
2
|
+
precisar decorar flags de comando."""
|
|
3
|
+
|
|
4
|
+
import questionary
|
|
5
|
+
from questionary import Style
|
|
6
|
+
from rich.console import Console
|
|
7
|
+
|
|
8
|
+
from abyss.ui.banner import section, show_banner
|
|
9
|
+
|
|
10
|
+
console = Console()
|
|
11
|
+
|
|
12
|
+
# Paleta terminal verde/preto, no mesmo espírito do Cerberus/ÆGIS
|
|
13
|
+
HACKER_STYLE = Style([
|
|
14
|
+
("qmark", "fg:#00ff41 bold"),
|
|
15
|
+
("question", "fg:#e0e0e0 bold"),
|
|
16
|
+
("answer", "fg:#00ff41 bold"),
|
|
17
|
+
("pointer", "fg:#00ff41 bold"),
|
|
18
|
+
("highlighted", "fg:#00ff41 bold"),
|
|
19
|
+
("selected", "fg:#00ff41"),
|
|
20
|
+
("separator", "fg:#666666"),
|
|
21
|
+
("instruction", "fg:#888888"),
|
|
22
|
+
("text", "fg:#e0e0e0"),
|
|
23
|
+
])
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def prompt_repo_path(default: str = "") -> str:
|
|
27
|
+
raw = questionary.path(
|
|
28
|
+
"Caminho do repositório alvo (Enter = diretório atual):",
|
|
29
|
+
default=default,
|
|
30
|
+
style=HACKER_STYLE,
|
|
31
|
+
).ask()
|
|
32
|
+
return raw.strip() if raw and raw.strip() else "."
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def main_menu() -> str:
|
|
36
|
+
show_banner()
|
|
37
|
+
choice = questionary.select(
|
|
38
|
+
"Selecione uma operação:",
|
|
39
|
+
choices=[
|
|
40
|
+
questionary.Choice("[1] Scan completo (secrets + dependências)", value="full"),
|
|
41
|
+
questionary.Choice("[2] Scan de secrets vazados", value="secrets"),
|
|
42
|
+
questionary.Choice("[3] Scan de dependências vulneráveis", value="deps"),
|
|
43
|
+
questionary.Choice("[4] Instalar/atualizar gitleaks", value="install_gitleaks"),
|
|
44
|
+
questionary.Choice("[0] Sair", value="exit"),
|
|
45
|
+
],
|
|
46
|
+
style=HACKER_STYLE,
|
|
47
|
+
qmark=">",
|
|
48
|
+
instruction=" ",
|
|
49
|
+
).ask()
|
|
50
|
+
return choice or "exit"
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def prompt_export_json() -> bool:
|
|
54
|
+
return questionary.confirm(
|
|
55
|
+
"Exportar relatório em JSON?",
|
|
56
|
+
default=False,
|
|
57
|
+
style=HACKER_STYLE,
|
|
58
|
+
).ask()
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def run_interactive():
|
|
62
|
+
from pathlib import Path
|
|
63
|
+
|
|
64
|
+
from abyss.cli import _print_dependency_findings, _print_secret_findings, _write_json
|
|
65
|
+
from abyss.scanners import dependencies, secrets
|
|
66
|
+
|
|
67
|
+
while True:
|
|
68
|
+
choice = main_menu()
|
|
69
|
+
|
|
70
|
+
if choice == "exit":
|
|
71
|
+
console.print("\n[dim green]encerrando sessão...[/dim green]\n")
|
|
72
|
+
break
|
|
73
|
+
|
|
74
|
+
if choice == "install_gitleaks":
|
|
75
|
+
from abyss.core.installer import install, is_installed
|
|
76
|
+
|
|
77
|
+
section("INSTALAÇÃO — GITLEAKS")
|
|
78
|
+
if is_installed():
|
|
79
|
+
console.print("[green]✓[/green] gitleaks já está instalado.")
|
|
80
|
+
else:
|
|
81
|
+
install(progress_callback=lambda msg: console.print(f"[dim]{msg}[/dim]"))
|
|
82
|
+
console.print("[green]✓[/green] gitleaks instalado com sucesso.")
|
|
83
|
+
questionary.press_any_key_to_continue(style=HACKER_STYLE).ask()
|
|
84
|
+
continue
|
|
85
|
+
|
|
86
|
+
repo_path = prompt_repo_path()
|
|
87
|
+
if not repo_path:
|
|
88
|
+
continue
|
|
89
|
+
|
|
90
|
+
resolved = str(Path(repo_path).resolve())
|
|
91
|
+
if not Path(resolved).exists():
|
|
92
|
+
console.print(f"[red]✗ caminho não encontrado:[/red] {resolved}")
|
|
93
|
+
questionary.press_any_key_to_continue(style=HACKER_STYLE).ask()
|
|
94
|
+
continue
|
|
95
|
+
|
|
96
|
+
secret_findings, dep_findings = [], []
|
|
97
|
+
|
|
98
|
+
if choice in ("full", "secrets"):
|
|
99
|
+
section("SCANNING — SECRETS")
|
|
100
|
+
with console.status("[green]procurando secrets vazados...[/green]"):
|
|
101
|
+
secret_findings = secrets.scan_repo(resolved, progress_callback=console.print)
|
|
102
|
+
_print_secret_findings(secret_findings)
|
|
103
|
+
|
|
104
|
+
if choice in ("full", "deps"):
|
|
105
|
+
section("SCANNING — DEPENDENCIES")
|
|
106
|
+
with console.status("[green]auditando dependências...[/green]"):
|
|
107
|
+
dep_findings = dependencies.scan_repo(resolved)
|
|
108
|
+
_print_dependency_findings(dep_findings)
|
|
109
|
+
|
|
110
|
+
total = len(secret_findings) + len(dep_findings)
|
|
111
|
+
section("RESULTADO")
|
|
112
|
+
if total == 0:
|
|
113
|
+
console.print("[bold green]✓ nenhum problema encontrado.[/bold green]")
|
|
114
|
+
else:
|
|
115
|
+
console.print(f"[bold yellow]⚠ {total} problema(s) encontrado(s).[/bold yellow]")
|
|
116
|
+
|
|
117
|
+
if prompt_export_json():
|
|
118
|
+
output_path = questionary.path(
|
|
119
|
+
"Salvar relatório em:", default="abyss-report.json", style=HACKER_STYLE
|
|
120
|
+
).ask()
|
|
121
|
+
if output_path:
|
|
122
|
+
_write_json(Path(output_path), secret_findings, dep_findings, resolved)
|
|
123
|
+
console.print(f"[dim]relatório salvo em {output_path}[/dim]")
|
|
124
|
+
|
|
125
|
+
questionary.press_any_key_to_continue(style=HACKER_STYLE).ask()
|
abyss/utils/__init__.py
ADDED
|
File without changes
|
abyss/utils/platform.py
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""Detecção de plataforma e arquitetura para baixar os binários corretos."""
|
|
2
|
+
|
|
3
|
+
import platform
|
|
4
|
+
import sys
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def get_os() -> str:
|
|
8
|
+
"""Retorna 'windows', 'darwin' ou 'linux'."""
|
|
9
|
+
system = platform.system().lower()
|
|
10
|
+
if system == "darwin":
|
|
11
|
+
return "darwin"
|
|
12
|
+
if system == "windows":
|
|
13
|
+
return "windows"
|
|
14
|
+
return "linux"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def get_arch() -> str:
|
|
18
|
+
"""Normaliza a arquitetura pro formato usado nos releases do gitleaks."""
|
|
19
|
+
machine = platform.machine().lower()
|
|
20
|
+
if machine in ("x86_64", "amd64"):
|
|
21
|
+
return "x64"
|
|
22
|
+
if machine in ("arm64", "aarch64"):
|
|
23
|
+
return "arm64"
|
|
24
|
+
if machine in ("i386", "i686", "x86"):
|
|
25
|
+
return "x32"
|
|
26
|
+
return machine
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def binary_name(base_name: str) -> str:
|
|
30
|
+
"""Adiciona .exe no Windows quando necessário."""
|
|
31
|
+
if get_os() == "windows":
|
|
32
|
+
return f"{base_name}.exe"
|
|
33
|
+
return base_name
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def abyss_home() -> "Path":
|
|
37
|
+
"""Diretório onde guardamos binários auxiliares baixados (gitleaks etc)."""
|
|
38
|
+
from pathlib import Path
|
|
39
|
+
|
|
40
|
+
if get_os() == "windows":
|
|
41
|
+
base = Path.home() / "AppData" / "Local" / "abyss-cli"
|
|
42
|
+
elif get_os() == "darwin":
|
|
43
|
+
base = Path.home() / "Library" / "Application Support" / "abyss-cli"
|
|
44
|
+
else:
|
|
45
|
+
base = Path.home() / ".local" / "share" / "abyss-cli"
|
|
46
|
+
|
|
47
|
+
base.mkdir(parents=True, exist_ok=True)
|
|
48
|
+
return base
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def is_supported() -> bool:
|
|
52
|
+
return sys.version_info >= (3, 9)
|
abyss/utils/redact.py
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""Garante que secrets encontrados nunca aparecem em texto puro em logs,
|
|
2
|
+
issues ou saída de terminal — só os últimos caracteres ficam visíveis."""
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def redact(secret: str, visible_chars: int = 4) -> str:
|
|
6
|
+
"""Mascara um secret, mantendo só os últimos N caracteres visíveis.
|
|
7
|
+
|
|
8
|
+
Exemplo: redact("ghp_aBcD1234efGH5678") -> "••••••••••••••••5678"
|
|
9
|
+
"""
|
|
10
|
+
if not secret:
|
|
11
|
+
return ""
|
|
12
|
+
|
|
13
|
+
if len(secret) <= visible_chars:
|
|
14
|
+
return "•" * len(secret)
|
|
15
|
+
|
|
16
|
+
masked_len = len(secret) - visible_chars
|
|
17
|
+
return ("•" * masked_len) + secret[-visible_chars:]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def redact_line(line: str, secret: str, visible_chars: int = 4) -> str:
|
|
21
|
+
"""Substitui a ocorrência do secret dentro de uma linha de código/log
|
|
22
|
+
pela versão mascarada, preservando o resto do contexto da linha."""
|
|
23
|
+
if secret not in line:
|
|
24
|
+
return line
|
|
25
|
+
return line.replace(secret, redact(secret, visible_chars))
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: abyss-scanner
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Abyss — CLI que mergulha nas profundezas do seu código pra achar secrets vazados e dependências vulneráveis
|
|
5
|
+
Author-email: Lohane <lohane.mdev@gmail.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Requires-Python: >=3.9
|
|
8
|
+
Requires-Dist: packaging>=24.0
|
|
9
|
+
Requires-Dist: pygithub>=2.3.0
|
|
10
|
+
Requires-Dist: questionary>=2.0.0
|
|
11
|
+
Requires-Dist: requests>=2.31.0
|
|
12
|
+
Requires-Dist: rich>=13.7.0
|
|
13
|
+
Requires-Dist: typer>=0.12.0
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
|
|
16
|
+
# abyss
|
|
17
|
+
|
|
18
|
+
> CLI que mergulha nas profundezas do seu código pra achar o que tava
|
|
19
|
+
> escondido: secrets vazados e dependências vulneráveis.
|
|
20
|
+
>
|
|
21
|
+
> Nome e mascote inspirados na lula-vampira-do-inferno
|
|
22
|
+
> (*Vampyroteuthis infernalis*) — criatura que vive nas profundezas onde
|
|
23
|
+
> não chega luz.
|
|
24
|
+
|
|
25
|
+
Multiplataforma (Windows, macOS, Linux). Escaneia repositórios em busca de:
|
|
26
|
+
|
|
27
|
+
- **Secrets vazados** (chaves de API, tokens, senhas commitadas) via
|
|
28
|
+
[gitleaks](https://github.com/gitleaks/gitleaks)
|
|
29
|
+
- **Dependências vulneráveis** em projetos Python (`pip-audit`) e Node
|
|
30
|
+
(`npm audit`)
|
|
31
|
+
|
|
32
|
+
## Instalação (dev)
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
pip install -e .
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Uso
|
|
39
|
+
|
|
40
|
+
Rodar sem argumento nenhum abre o menu interativo:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
abyss
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Ou direto por linha de comando:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
abyss scan . # escaneia o diretório atual
|
|
50
|
+
abyss scan /caminho/pro/repo # escaneia outro caminho
|
|
51
|
+
abyss scan . --secrets-only # só secrets
|
|
52
|
+
abyss scan . --deps-only # só dependências
|
|
53
|
+
abyss scan . --json report.json # salva relatório em JSON
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Na primeira execução, o binário do `gitleaks` é baixado automaticamente
|
|
57
|
+
para a plataforma atual e guardado em cache local (não precisa instalar
|
|
58
|
+
nada manualmente).
|
|
59
|
+
|
|
60
|
+
Para o scan de dependências, `pip-audit` (Python) e `npm` (Node) precisam
|
|
61
|
+
estar disponíveis no PATH quando aplicável — se não estiverem, aquele
|
|
62
|
+
scan é pulado silenciosamente.
|
|
63
|
+
|
|
64
|
+
## Segurança
|
|
65
|
+
|
|
66
|
+
Secrets encontrados **nunca** aparecem em texto puro — são sempre
|
|
67
|
+
redigidos (`redact()`), mostrando só os últimos 4 caracteres. Isso vale
|
|
68
|
+
pra saída de terminal, JSON exportado e qualquer log.
|
|
69
|
+
|
|
70
|
+
## Mascote
|
|
71
|
+
|
|
72
|
+
O ASCII art da lula-vampira fica em `abyss/ui/banner.py`, na constante
|
|
73
|
+
`MASCOT_ART` — troque o placeholder pelo desenho final quando estiver
|
|
74
|
+
pronto.
|
|
75
|
+
|
|
76
|
+
## Roadmap
|
|
77
|
+
|
|
78
|
+
- [ ] Publicar no PyPI
|
|
79
|
+
- [ ] Integração com GitHub API — abrir issue automática ao encontrar problema
|
|
80
|
+
- [ ] Suporte a mais ecossistemas (Cargo, Go modules)
|
|
81
|
+
- [ ] Modo `--watch` para rodar em CI/pre-commit hook
|
|
82
|
+
- [x] Nome definitivo: **Abyss**
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
abyss/__init__.py,sha256=kUR5RAFc7HCeiqdlX36dZOHkUI5wI6V_43RpEcD8b-0,22
|
|
2
|
+
abyss/cli.py,sha256=l0uctrGhwbfpd4jCgr0RgP_L9epzm-ozIw_k9ybM2Ts,4679
|
|
3
|
+
abyss/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
4
|
+
abyss/core/installer.py,sha256=5PJR7KDwUvtFMFUMHRmmzbLcXZKSp8ypkgIqjQWd2vY,2480
|
|
5
|
+
abyss/scanners/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
6
|
+
abyss/scanners/dependencies.py,sha256=aI89az-E3HN1sF36mUWd9Zl9edYzSAVHz_2FBhOBdeI,3801
|
|
7
|
+
abyss/scanners/secrets.py,sha256=B4vG703jH-1oX0PD6TY0rHGtJC8NHjBdRx6_bBhRGYk,2303
|
|
8
|
+
abyss/ui/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
9
|
+
abyss/ui/banner.py,sha256=JwtRa3E0n-uPi-_Li5gKxKF7qlQ76ts7ngWLugwgxAc,4897
|
|
10
|
+
abyss/ui/menu.py,sha256=PXj3PJAg31wL6HLWqTteNoV1SLZhl3KJTLKkGcVYrPI,4497
|
|
11
|
+
abyss/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
12
|
+
abyss/utils/platform.py,sha256=5lbY7bRtos081KHaiv6t0FJ5cME44XGhAPtEX4Pyrpo,1418
|
|
13
|
+
abyss/utils/redact.py,sha256=cH5gl6YA3DZ5R8bFwfRpgu4lC7DLExFRgo1LteXFQkc,943
|
|
14
|
+
abyss_scanner-0.1.0.dist-info/METADATA,sha256=CrX9qPhRl1qKrJHeqwUKI8Y4to_Gmdxrkbo2kO-UCgE,2507
|
|
15
|
+
abyss_scanner-0.1.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
|
|
16
|
+
abyss_scanner-0.1.0.dist-info/entry_points.txt,sha256=b0mou0ErXggbYjNAdGJjQcnNBCXVrpDtRfFkyrMeB3s,40
|
|
17
|
+
abyss_scanner-0.1.0.dist-info/RECORD,,
|