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
50 changes: 50 additions & 0 deletions src/agent_rules_kit/governance.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@
"Instruction file appears to encourage bypassing review, CI, or safe integration boundaries."
)

SECRET_BOUNDARY_RULE_ID = "AIRK-GOV002"
SECRET_BOUNDARY_MESSAGE = "Instruction file may lack an explicit secret-handling boundary."

UNSUPPORTED_CLAIM_RULE_ID = "AIRK-GOV006"
UNSUPPORTED_CLAIM_MESSAGE = (
"Instruction file may contain an unsupported security or maturity claim."
Expand Down Expand Up @@ -59,6 +62,16 @@
),
)

SECRET_BOUNDARY_PATTERNS: tuple[Pattern[str], ...] = (
re.compile(r"\bsecret(?:s)?\b", re.IGNORECASE),
re.compile(r"\btoken(?:s)?\b", re.IGNORECASE),
re.compile(r"\bcredential(?:s)?\b", re.IGNORECASE),
re.compile(r"\bapi[-_ ]?key(?:s)?\b", re.IGNORECASE),
re.compile(r"\bprivate\s+(data|url(?:s)?|key(?:s)?)\b", re.IGNORECASE),
re.compile(r"\bcustomer\s+data\b", re.IGNORECASE),
re.compile(r"\bsensitive\s+(value(?:s)?|information|data)\b", re.IGNORECASE),
)

UNSUPPORTED_CLAIM_PATTERNS: tuple[Pattern[str], ...] = (
re.compile(r"\bguarantee[sd]?\s+(security|safety)\b", re.IGNORECASE),
re.compile(r"\bguaranteed\s+(secure|safe|security|safety)\b", re.IGNORECASE),
Expand Down Expand Up @@ -101,9 +114,38 @@ def find_governance_findings(
return (
*find_unsupported_claim_findings(repository_root, instruction_files),
*find_review_ci_bypass_findings(repository_root, instruction_files),
*find_missing_secret_boundary_findings(repository_root, instruction_files),
)


def find_missing_secret_boundary_findings(
repository_root: Path,
instruction_files: tuple[InstructionFile, ...],
) -> tuple[Finding, ...]:
"""Return findings for files without visible secret-handling guidance."""
findings: list[Finding] = []

for instruction_file in instruction_files:
candidate = repository_root / instruction_file.path

try:
text = candidate.read_text(encoding="utf-8")
except UnicodeDecodeError:
continue

if not _contains_secret_boundary(text):
findings.append(
Finding(
rule_id=SECRET_BOUNDARY_RULE_ID,
severity=Severity.WARNING,
message=SECRET_BOUNDARY_MESSAGE,
path=instruction_file.path,
)
)

return tuple(findings)


def find_review_ci_bypass_findings(
repository_root: Path,
instruction_files: tuple[InstructionFile, ...],
Expand Down Expand Up @@ -168,6 +210,10 @@ def _find_line_findings(
return tuple(findings)


def _contains_secret_boundary(text: str) -> bool:
return any(pattern.search(text) is not None for pattern in SECRET_BOUNDARY_PATTERNS)


def _contains_review_ci_bypass_guidance(line: str) -> bool:
has_bypass_guidance = any(
pattern.search(line) is not None for pattern in REVIEW_CI_BYPASS_PATTERNS
Expand Down Expand Up @@ -200,10 +246,14 @@ def _contains_unsupported_claim(line: str) -> bool:
"REVIEW_CI_BYPASS_MESSAGE",
"REVIEW_CI_BYPASS_PATTERNS",
"REVIEW_CI_BYPASS_RULE_ID",
"SECRET_BOUNDARY_MESSAGE",
"SECRET_BOUNDARY_PATTERNS",
"SECRET_BOUNDARY_RULE_ID",
"UNSUPPORTED_CLAIM_MESSAGE",
"UNSUPPORTED_CLAIM_PATTERNS",
"UNSUPPORTED_CLAIM_RULE_ID",
"find_governance_findings",
"find_missing_secret_boundary_findings",
"find_review_ci_bypass_findings",
"find_unsupported_claim_findings",
]
7 changes: 7 additions & 0 deletions tests/fixtures/repositories/missing-secret-boundary/AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# AGENTS.md

Rules:

- Read relevant files before editing.
- Run local checks before committing.
- Use pull requests for changes to main.
1 change: 1 addition & 0 deletions tests/fixtures/repositories/unsupported-claim/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ Rules:

- This process guarantees security for the repository.
- This project is enterprise-grade and production-ready.
- Do not commit secrets, tokens, credentials, private URLs, or customer data.
25 changes: 25 additions & 0 deletions tests/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -533,6 +533,31 @@ def test_check_json_reports_review_ci_bypass_findings(self) -> None:
)


def test_check_json_reports_missing_secret_boundary_findings(self) -> None:
output = io.StringIO()

with redirect_stdout(output):
exit_code = main(
[
"check",
str(FIXTURE_ROOT / "missing-secret-boundary"),
"--format",
"json",
]
)

payload = json.loads(output.getvalue())

self.assertEqual(exit_code, 0)
self.assertEqual(payload["summary"]["finding_count"], 1)
self.assertEqual(
[finding["rule_id"] for finding in payload["findings"]],
["AIRK-GOV002"],
)
self.assertEqual(payload["findings"][0]["path"], "AGENTS.md")
self.assertNotIn("line", payload["findings"][0])



if __name__ == "__main__":
unittest.main()
53 changes: 51 additions & 2 deletions tests/test_governance.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from agent_rules_kit.discovery import discover_instruction_files
from agent_rules_kit.governance import (
find_governance_findings,
find_missing_secret_boundary_findings,
find_review_ci_bypass_findings,
find_unsupported_claim_findings,
)
Expand Down Expand Up @@ -137,6 +138,54 @@ def test_ignores_negative_guidance_about_review_ci_bypass(self) -> None:

self.assertEqual(findings, ())


def test_reports_missing_secret_handling_boundary(self) -> None:
with tempfile.TemporaryDirectory() as temporary_directory:
repository = Path(temporary_directory)
(repository / "AGENTS.md").write_text(
"\n".join(
[
"# AGENTS.md",
"",
"Rules:",
"",
"- Read relevant files before editing.",
"- Run local checks before committing.",
]
)
+ "\n",
encoding="utf-8",
)

instruction_files = discover_instruction_files(repository)
findings = find_missing_secret_boundary_findings(repository, instruction_files)

self.assertEqual([finding.rule_id for finding in findings], ["AIRK-GOV002"])
self.assertEqual([finding.path for finding in findings], ["AGENTS.md"])
self.assertEqual([finding.line for finding in findings], [None])

def test_ignores_files_with_secret_handling_boundary(self) -> None:
with tempfile.TemporaryDirectory() as temporary_directory:
repository = Path(temporary_directory)
(repository / "AGENTS.md").write_text(
"\n".join(
[
"# AGENTS.md",
"",
"Rules:",
"",
"- Do not commit secrets, tokens, credentials, private URLs, or customer data.",
]
)
+ "\n",
encoding="utf-8",
)

instruction_files = discover_instruction_files(repository)
findings = find_missing_secret_boundary_findings(repository, instruction_files)

self.assertEqual(findings, ())

def test_governance_findings_keep_stable_rule_order(self) -> None:
with tempfile.TemporaryDirectory() as temporary_directory:
repository = Path(temporary_directory)
Expand All @@ -160,9 +209,9 @@ def test_governance_findings_keep_stable_rule_order(self) -> None:

self.assertEqual(
[finding.rule_id for finding in findings],
["AIRK-GOV006", "AIRK-GOV003"],
["AIRK-GOV006", "AIRK-GOV003", "AIRK-GOV002"],
)
self.assertEqual([finding.line for finding in findings], [5, 6])
self.assertEqual([finding.line for finding in findings], [5, 6, None])



Expand Down
Loading