Skip to content

Commit a16413d

Browse files
committed
Harden command-policy parity validation
1 parent 1f478d7 commit a16413d

2 files changed

Lines changed: 104 additions & 4 deletions

File tree

.codex/lib/validate_rules.py

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
import argparse
77
import json
8+
import re
89
import sys
910
import tomllib
1011
from pathlib import Path
@@ -16,6 +17,26 @@
1617
"git_branch": "git-branch",
1718
}
1819

20+
REQUIRED_FORBIDDEN_COMMAND_EXAMPLES = (
21+
"rm -rf",
22+
"git reset --hard",
23+
"git clean -f",
24+
"git clean -fd",
25+
"git clean -fx",
26+
"git clean -fdx",
27+
"git push --force",
28+
"git push -f",
29+
"sudo",
30+
"chmod 777",
31+
"cat ~/.ssh",
32+
"cat .env",
33+
"cat *.env",
34+
"cat ~/.env",
35+
"type .env",
36+
"type *.env",
37+
"type ~/.env",
38+
)
39+
1940

2041
def emit(root: Path, errors: list[str], warnings: list[str]) -> int:
2142
status = "pass" if not errors else "fail"
@@ -85,9 +106,17 @@ def main() -> int:
85106
errors.append(".codex/rules/settings.rules: match is a prefix_rule field, not a top-level function")
86107
if 'decision = "deny"' in text:
87108
errors.append('.codex/rules/settings.rules: use decision = "forbidden", not "deny"')
88-
for required in ("git reset --hard", "rm -rf", "force push"):
89-
if required not in text:
90-
warnings.append(f".codex/rules/settings.rules: missing example text for {required}")
109+
if 'decision = "forbidden"' not in text:
110+
errors.append('.codex/rules/settings.rules: missing decision = "forbidden" for denied command policy')
111+
quoted_examples = set(re.findall(r'"([^"]*)"', text))
112+
missing_forbidden_examples = [
113+
example for example in REQUIRED_FORBIDDEN_COMMAND_EXAMPLES if example not in quoted_examples
114+
]
115+
if missing_forbidden_examples:
116+
errors.append(
117+
".codex/rules/settings.rules: missing parity-critical forbidden command coverage for "
118+
+ ", ".join(missing_forbidden_examples)
119+
)
91120
else:
92121
warnings.append(".codex/rules/settings.rules not present yet")
93122

.codex/rules/settings.rules

Lines changed: 72 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ prefix_rule(
1111
pattern = ["rm", "-rf"],
1212
decision = "forbidden",
1313
justification = "Use a scoped non-destructive command, or ask the user for an explicit cleanup plan.",
14-
match = ["rm -rf build"],
14+
match = ["rm -rf"],
1515
)
1616
prefix_rule(
1717
pattern = ["git", "reset", "--hard"],
@@ -25,21 +25,92 @@ prefix_rule(
2525
justification = "Use git status and ask the user before removing untracked files.",
2626
match = ["git clean -fdx"],
2727
)
28+
prefix_rule(
29+
pattern = ["git", "clean", "-f"],
30+
decision = "forbidden",
31+
justification = "Use git status and ask the user before removing untracked files.",
32+
match = ["git clean -f"],
33+
)
34+
prefix_rule(
35+
pattern = ["git", "clean", "-fd"],
36+
decision = "forbidden",
37+
justification = "Use git status and ask the user before removing untracked files.",
38+
match = ["git clean -fd"],
39+
)
40+
prefix_rule(
41+
pattern = ["git", "clean", "-fx"],
42+
decision = "forbidden",
43+
justification = "Use git status and ask the user before removing untracked files.",
44+
match = ["git clean -fx"],
45+
)
2846
prefix_rule(
2947
pattern = ["git", "push", "--force"],
3048
decision = "forbidden",
3149
justification = "Do not force push or rewrite remote history without explicit user approval.",
3250
match = ["git push --force"],
3351
)
52+
prefix_rule(
53+
pattern = ["git", "push", "-f"],
54+
decision = "forbidden",
55+
justification = "Do not force push or rewrite remote history without explicit user approval.",
56+
match = ["git push -f"],
57+
)
58+
prefix_rule(
59+
pattern = ["sudo"],
60+
decision = "forbidden",
61+
justification = "Do not escalate privileges from project automation.",
62+
match = ["sudo"],
63+
)
64+
prefix_rule(
65+
pattern = ["chmod", "777"],
66+
decision = "forbidden",
67+
justification = "Use the narrowest file mode required instead of world-writable permissions.",
68+
match = ["chmod 777"],
69+
)
3470
prefix_rule(
3571
pattern = ["cat", "~/.ssh"],
3672
decision = "forbidden",
3773
justification = "Do not read local SSH secrets.",
3874
match = ["cat ~/.ssh"],
3975
)
76+
77+
# Prefix rules can cover direct tokenized .env reads, including the literal
78+
# shell glob token users type before expansion. Upstream .env redirection or
79+
# write patterns are outside this syntax and must stay handled by prompts,
80+
# hooks, or manual review rather than unsupported redirection rules.
81+
prefix_rule(
82+
pattern = ["cat", ".env"],
83+
decision = "forbidden",
84+
justification = "Do not read local environment secret files.",
85+
match = ["cat .env"],
86+
)
87+
prefix_rule(
88+
pattern = ["cat", "*.env"],
89+
decision = "forbidden",
90+
justification = "Do not read local environment secret files.",
91+
match = ["cat *.env"],
92+
)
4093
prefix_rule(
4194
pattern = ["cat", "~/.env"],
4295
decision = "forbidden",
4396
justification = "Do not read local environment secret files.",
4497
match = ["cat ~/.env"],
4598
)
99+
prefix_rule(
100+
pattern = ["type", ".env"],
101+
decision = "forbidden",
102+
justification = "Do not read local environment secret files.",
103+
match = ["type .env"],
104+
)
105+
prefix_rule(
106+
pattern = ["type", "*.env"],
107+
decision = "forbidden",
108+
justification = "Do not read local environment secret files.",
109+
match = ["type *.env"],
110+
)
111+
prefix_rule(
112+
pattern = ["type", "~/.env"],
113+
decision = "forbidden",
114+
justification = "Do not read local environment secret files.",
115+
match = ["type ~/.env"],
116+
)

0 commit comments

Comments
 (0)