fix(security_guards): reject un-allowlisted .gitignore negations (#195)

check_gitignore() verified each required personal-data rule was present via set membership, but .gitignore is order-sensitive: a later !pattern re-includes a file an earlier rule excluded, so the required line stays physically present while the file is no longer ignored - the guard failed open on exactly the weakening its docstring claims to catch. Keeps the required-rules-present check and additionally rejects any negation line outside a small reviewed ALLOWED_IGNORE_NEGATIONS allowlist (same explicit-widening pattern as ALLOWED_PERMISSIONS). Fixes #194.

By @thejesh23. Verified: allowlist matches the four negations currently in .gitignore; guard test suite passes locally (17 tests) and in CI.

Closes #194
This commit is contained in:
Thejesh Reddy
2026-07-20 18:46:58 +02:00
committed by GitHub
parent 669f5ac1ab
commit 36462e356e
2 changed files with 52 additions and 4 deletions
+23
View File
@@ -125,6 +125,29 @@ class GitignoreGuardTests(GuardRepoFixture):
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
class GitignoreNegationTests(GuardRepoFixture):
def test_negation_reincluding_personal_data_fails(self):
# .gitignore is order-sensitive: `!salary_data.json` after the
# `salary_data.json` rule re-includes the file, so the required rule is
# still present but no longer takes effect. Set membership on the
# required rules cannot see this, so the negation must be rejected.
self.write_gitignore(list(security_guards.REQUIRED_IGNORE_RULES) + ["!salary_data.json"])
result = run_guards(self.root)
self.assertEqual(result.returncode, 1, result.stdout + result.stderr)
self.assertIn("negation rule not in the reviewed allowlist", result.stdout)
self.assertIn("!salary_data.json", result.stdout)
def test_allowlisted_negations_pass(self):
# The template's own benign negations (example CV/cover letter, fonts,
# .gitkeep placeholders) must keep passing.
self.write_gitignore(
list(security_guards.REQUIRED_IGNORE_RULES)
+ sorted(security_guards.ALLOWED_IGNORE_NEGATIONS)
)
result = run_guards(self.root)
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
class ManifestGuardTests(GuardRepoFixture):
def test_each_lifecycle_script_fails(self):
for script in sorted(security_guards.FORBIDDEN_SCRIPTS):