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
44 changes: 41 additions & 3 deletions src/becwright/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -138,31 +138,53 @@ def _cmd_list(_: argparse.Namespace) -> int:


_SKIP_DIRS = {".git", "node_modules", ".venv", "venv", "__pycache__", "dist", "build", ".tox"}
_EXT_LANG = {".py": "python", ".js": "js", ".ts": "ts"}
_EXT_LANG = {
".py": "python",
".js": "js",
".ts": "ts",
".go": "go",
".rs": "rust",
}


def _detect_languages(root: Path) -> list[str]:
found: set[str] = set()

for path in root.rglob("*"):
if not path.is_file():
continue
if any(part in _SKIP_DIRS for part in path.relative_to(root).parts):
continue

lang = _EXT_LANG.get(path.suffix)
if lang:
found.add(lang)
return [lang for lang in ("python", "js", "ts") if lang in found]

return [lang for lang in ("python", "js", "ts", "go", "rust") if lang in found]


def _starter_rules(langs: list[str]) -> list[dict]:
source_globs = [g for lang, g in (("python", "**/*.py"), ("js", "**/*.js"), ("ts", "**/*.ts")) if lang in langs]
source_globs = [
g
for lang, g in (
("python", "**/*.py"),
("js", "**/*.js"),
("ts", "**/*.ts"),
("go", "**/*.go"),
("rust", "**/*.rs"),
)
if lang in langs
]

rules: list[dict] = []

if source_globs:
rules.append(dict(
id="no-hardcoded-secrets", paths=source_globs, severity="blocking",
check="becwright run hardcoded_secrets",
intent="No secret (key, token, password) should be hardcoded in the code.",
why="A secret in the repo stays in git history forever and is visible to anyone with access to the code."))

if "python" in langs:
rules.append(dict(
id="no-debug-remnants", paths=["**/*.py"], severity="blocking",
Expand All @@ -174,6 +196,7 @@ def _starter_rules(langs: list[str]) -> list[dict]:
check="becwright run dangerous_eval",
intent="Avoid eval and exec, which run arbitrary code.",
why="Dynamic eval/exec on untrusted input is remote code execution."))

if "js" in langs or "ts" in langs:
js_globs = [g for g in source_globs if g.endswith((".js", ".ts"))]
rules.append(dict(
Expand All @@ -186,6 +209,21 @@ def _starter_rules(langs: list[str]) -> list[dict]:
check="becwright run forbid --pattern 'console\\.log\\s*\\('",
intent="Avoid 'console.log(...)' in JavaScript/TypeScript code.",
why="Debug console.log statements clutter production output."))

if "go" in langs:
rules.append(dict(
id="no-debug-go", paths=["**/*.go"], severity="blocking",
check=r"becwright run forbid --pattern 'fmt\.Println\s*\(|panic\s*\('",
intent="Do not leave debug output or panic calls in Go code.",
why="Debug statements and unexpected panic calls should not reach production."))

if "rust" in langs:
rules.append(dict(
id="no-debug-rust", paths=["**/*.rs"], severity="blocking",
check=r"becwright run forbid --pattern 'dbg!\s*\(|println!\s*\('",
intent="Do not leave debug output in Rust code.",
why="Debug macros and leftover println! calls should not reach production."))

return rules


Expand Down
63 changes: 63 additions & 0 deletions tests/test_init.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,16 @@ def test_detect_languages_none(tmp_path):
(tmp_path / "README.md").write_text("# hi\n", encoding="utf-8")
assert cli._detect_languages(tmp_path) == []

def test_detect_languages_go(tmp_path):
(tmp_path / "main.go").write_text("package main\n", encoding="utf-8")

assert cli._detect_languages(tmp_path) == ["go"]


def test_detect_languages_rust(tmp_path):
(tmp_path / "main.rs").write_text("fn main() {}\n", encoding="utf-8")

assert cli._detect_languages(tmp_path) == ["rust"]

# --- starter rules ---

Expand All @@ -44,13 +54,66 @@ def test_starter_rules_js():
ids = [r["id"] for r in cli._starter_rules(["js"])]
assert ids == ["no-hardcoded-secrets", "no-debugger-js", "no-console-log-js"]

def test_starter_rules_go():
rules = cli._starter_rules(["go"])

assert [rule["id"] for rule in rules] == [
"no-hardcoded-secrets",
"no-debug-go",
]

secrets_rule = rules[0]
debug_rule = rules[1]

assert secrets_rule["paths"] == ["**/*.go"]
assert debug_rule["paths"] == ["**/*.go"]
assert debug_rule["severity"] == "blocking"
assert debug_rule["check"] == (
r"becwright run forbid --pattern 'fmt\.Println\s*\(|panic\s*\('"
)


def test_starter_rules_rust():
rules = cli._starter_rules(["rust"])

assert [rule["id"] for rule in rules] == [
"no-hardcoded-secrets",
"no-debug-rust",
]

secrets_rule = rules[0]
debug_rule = rules[1]

assert secrets_rule["paths"] == ["**/*.rs"]
assert debug_rule["paths"] == ["**/*.rs"]
assert debug_rule["severity"] == "blocking"
assert debug_rule["check"] == (
r"becwright run forbid --pattern 'dbg!\s*\(|println!\s*\('"
)

def test_starter_rules_empty():
assert cli._starter_rules([]) == []


# --- rendering ---

def test_render_yaml_parses_go_and_rust_forbid_rules(tmp_path):
rules_path = tmp_path / "rules.yaml"

rules_path.write_text(
cli._render_rules_yaml(cli._starter_rules(["go", "rust"])),
encoding="utf-8",
)

rules = load_rules(rules_path)
rule_ids = {rule.id for rule in rules}

assert rule_ids == {
"no-hardcoded-secrets",
"no-debug-go",
"no-debug-rust",
}

def test_render_yaml_parses_and_keeps_forbid(tmp_path):
p = tmp_path / "rules.yaml"
p.write_text(cli._render_rules_yaml(cli._starter_rules(["js"])), encoding="utf-8")
Expand Down