Skip to content
Open
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
40 changes: 38 additions & 2 deletions cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import asyncio
import json
import os
import shutil
import sys
from datetime import datetime
from typing import Any, Dict, List, Optional
Expand Down Expand Up @@ -87,19 +88,50 @@ def _env_is_set(name: str) -> bool:
return bool(value)


# Modules that run a separate program instead of needing a key. Without it they are reported
# `skipped` at scan time, so the listing has to say whether it was found.
REQUIRED_BINARY: Dict[str, str] = {
"maigret": "maigret",
}


def _find_maigret() -> Optional[str]:
"""Where maigret would be run from, looked up the way modules/maigret_wrapper.py does it:
MAIGRET_BIN first, then the project's venv-maigret, then PATH. Nothing is executed."""
custom = os.getenv("MAIGRET_BIN")
if custom and os.path.isfile(custom):
return custom
project_root = os.path.dirname(os.path.abspath(__file__))
for path in (
os.path.join(project_root, "venv-maigret", "bin", "maigret"),
os.path.join(project_root, "venv-maigret", "Scripts", "maigret.exe"),
):
if os.path.isfile(path):
return path
return shutil.which("maigret")


_BINARY_FINDERS = {"maigret": _find_maigret}


def describe_modules(scan_type: Optional[str] = None) -> Dict[str, List[Dict[str, Any]]]:
types = [scan_type] if scan_type else list(MODULES_BY_TARGET_TYPE)
out: Dict[str, List[Dict[str, Any]]] = {}
for t in types:
rows = []
for name in MODULES_BY_TARGET_TYPE[t]:
env = REQUIRED_ENV.get(name) or OPTIONAL_ENV.get(name) or ()
rows.append({
row: Dict[str, Any] = {
"name": name,
"env": list(env),
"key": "required" if name in REQUIRED_ENV else ("optional" if env else None),
"configured": any(_env_is_set(v) for v in env) if env else None,
})
}
binary = REQUIRED_BINARY.get(name)
if binary is not None:
row["requires"] = binary
row["found"] = _BINARY_FINDERS[binary]() is not None
rows.append(row)
out[t] = rows
return out

Expand Down Expand Up @@ -502,6 +534,10 @@ def run_modules(args: argparse.Namespace) -> int:
for scan_type, rows in listing.items():
print(scan_type)
for row in rows:
if "requires" in row:
state = "found" if row["found"] else "binary not found, skipped at scan time"
print(f" {row['name']:<18} {'program: ' + row['requires']:<36} {state}")
continue
if not row["env"]:
print(f" {row['name']}")
continue
Expand Down
54 changes: 53 additions & 1 deletion tests/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -270,4 +270,56 @@ def test_modules_enabled_flag_needs_a_truthy_value(monkeypatch):

rows = {row["name"]: row for row in cli.describe_modules("email")["email"]}

assert rows["hudsonrock"]["configured"] is False
assert rows["hudsonrock"]["configured"] is False

def _no_maigret(monkeypatch):
monkeypatch.delenv("MAIGRET_BIN", raising=False)
monkeypatch.setattr(cli.shutil, "which", lambda name: None)


def test_modules_reports_a_missing_maigret(monkeypatch, capsys):
_no_maigret(monkeypatch)

with pytest.raises(SystemExit) as exc:
cli.main(["modules", "--type", "username"])

assert exc.value.code == 0
lines = {line.split()[0]: line for line in capsys.readouterr().out.splitlines()[1:]}
assert "program: maigret" in lines["maigret"]
assert lines["maigret"].endswith("binary not found, skipped at scan time")
assert lines["blackbird"].strip() == "blackbird"


def test_modules_json_carries_the_maigret_binary(monkeypatch, capsys):
import json

_no_maigret(monkeypatch)

with pytest.raises(SystemExit) as exc:
cli.main(["modules", "--type", "username", "--json"])

assert exc.value.code == 0
rows = {row["name"]: row for row in json.loads(capsys.readouterr().out)["username"]}
assert rows["maigret"]["requires"] == "maigret"
assert rows["maigret"]["found"] is False
assert "requires" not in rows["blackbird"]


def test_modules_finds_maigret_on_path(monkeypatch):
monkeypatch.delenv("MAIGRET_BIN", raising=False)
monkeypatch.setattr(cli.shutil, "which", lambda name: "/usr/bin/maigret" if name == "maigret" else None)

rows = {row["name"]: row for row in cli.describe_modules("username")["username"]}

assert rows["maigret"]["found"] is True


def test_modules_finds_maigret_through_maigret_bin(monkeypatch, tmp_path):
binary = tmp_path / "maigret"
binary.write_text("")
_no_maigret(monkeypatch)
monkeypatch.setenv("MAIGRET_BIN", str(binary))

rows = {row["name"]: row for row in cli.describe_modules("username")["username"]}

assert rows["maigret"]["found"] is True
Loading