Skip to content

Commit dab049c

Browse files
committed
!
1 parent 2606a45 commit dab049c

9 files changed

Lines changed: 260 additions & 13 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,3 +177,4 @@ openaudit.egg-info/PKG-INFO
177177
openaudit.egg-info/requires.txt
178178
openaudit.egg-info/SOURCES.txt
179179
openaudit.egg-info/top_level.txt
180+
/plans

.openaudit/custom_rules.yaml

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
# OpenAuditKit Custom Rules Template
2+
# Place this file in:
3+
# - Project root: .openaudit/custom_rules.yaml
4+
# - Home directory: ~/.openaudit/custom_rules.yaml
5+
#
6+
# Rules will be automatically loaded and merged with built-in rules.
7+
8+
rules:
9+
# Example: Detect specific company-wide patterns
10+
- id: "CUSTOM_COMPANY_API_KEY"
11+
description: "Company-specific API key pattern detected"
12+
regex: "MYCOMPANY_API_[A-Za-z0-9]{32}"
13+
entropy_check: false
14+
severity: "high"
15+
confidence: "high"
16+
category: "secret"
17+
remediation: "Move to environment variables or secrets manager."
18+
19+
# Example: Detect internal function misuse
20+
- id: "CUSTOM_DEPRECATED_FUNCTION"
21+
description: "Deprecated internal function usage"
22+
regex: "legacy_auth\\(|old_database_connect\\("
23+
severity: "medium"
24+
confidence: "high"
25+
category: "technical_debt"
26+
remediation: "Migrate to the new authentication/database API."
27+
28+
# Example: Detect hardcoded internal URLs
29+
- id: "CUSTOM_INTERNAL_URL"
30+
description: "Hardcoded internal service URL"
31+
regex: "https?://(dev|staging|internal)\\.(mycompany|example)\\.com"
32+
severity: "medium"
33+
confidence: "medium"
34+
category: "config"
35+
remediation: "Use environment-based configuration."
36+
37+
# Example: Detect insecure coding patterns
38+
- id: "CUSTOM_EVAL_USAGE"
39+
description: "Usage of eval() or exec() detected"
40+
regex: "\\beval\\(|\\bexec\\("
41+
severity: "critical"
42+
confidence: "high"
43+
category: "security"
44+
remediation: "Avoid eval/exec. Use safer alternatives like ast.literal_eval()."
45+
46+
# Example: Detect TODO patterns with specific team tags
47+
- id: "CUSTOM_BLOCKED_TODO"
48+
description: "Blocked TODO found (requires immediate attention)"
49+
regex: "TODO:\\s*BLOCKED:"
50+
severity: "high"
51+
confidence: "high"
52+
category: "technical_debt"
53+
remediation: "Resolve the blocking issue or escalate to team lead."
54+
55+
# Example: Entropy-based secret detection
56+
- id: "CUSTOM_HIGH_ENTROPY_SECRET"
57+
description: "High-entropy string that might be a secret"
58+
regex: "secret[_-]?key[\"']?\\s*[:=]\\s*[\"']([A-Za-z0-9+/=]{20,})[\"']"
59+
entropy_check: true
60+
severity: "high"
61+
confidence: "medium"
62+
category: "secret"
63+
remediation: "Verify if this is a real secret. If yes, move to secrets manager."

README.md

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,36 @@ Prevent security issues from being committed by using our pre-configured git hoo
111111

112112
Now, `openaudit` will automatically scan your code before every commit!
113113

114+
## 🎨 Custom Rules ("Vibe Checks")
115+
116+
Define your own security and quality rules that match your team's standards.
117+
118+
1. **Create a custom rules file**:
119+
```bash
120+
openaudit custom-rules init
121+
```
122+
123+
2. **Edit the generated file** (`.openaudit/custom_rules.yaml`):
124+
```yaml
125+
rules:
126+
- id: "CUSTOM_COMPANY_SECRET"
127+
description: "Company API key detected"
128+
regex: "MYCOMPANY_[A-Z0-9]{32}"
129+
severity: "high"
130+
confidence: "high"
131+
category: "secret"
132+
remediation: "Move to secrets manager."
133+
```
134+
135+
3. **Run scan** - Custom rules are automatically loaded:
136+
```bash
137+
openaudit scan .
138+
```
139+
140+
Custom rules can be placed in:
141+
- **Project-level**: `.openaudit/custom_rules.yaml` (committed to repo)
142+
- **User-level**: `~/.openaudit/custom_rules.yaml` (personal rules)
143+
114144
## 🛡️ VibeGuard (Tech Debt Scanner)
115145
116146
We know "vibecodin" happens. OpenAuditKit now includes **VibeGuard**, a scanner designed to catch non-engineering practices before they hit production:

memory_bank.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,12 @@
4242
- **Implementation**: Added `.pre-commit-config.yaml` and `pre-commit` dev dependency.
4343
- **Usage**: `pre-commit install` sets up the hook.
4444

45+
### 6. Custom Rules System 🎨
46+
- **Objective**: Allow users to define custom "Vibe Checks" for their projects.
47+
- **Implementation**: Extended `RulesEngine` to load rules from `.openaudit/custom_rules.yaml` and `~/.openaudit/custom_rules.yaml`.
48+
- **CLI Commands**: `openaudit custom-rules init`, `openaudit custom-rules list`.
49+
- **Integration**: All scanners now support custom rules (SecretScanner, TechDebtScanner, etc.).
50+
4551
## 🛠️ Known Technical Debt & TODOs
4652

4753
### 1. Windows Encoding Support (FIXED)
@@ -63,4 +69,4 @@
6369
## 🔮 Future Roadmap
6470
- [ ] **IDE Extensions**: VSCode extension using the MCP server.
6571
- [x] **Git Hooks**: Pre-commit hook added (`.pre-commit-config.yaml`).
66-
- [ ] **Custom Rules**: Allow users to define custom "Vibe Checks" (rules).
72+
- [x] **Custom Rules**: Implemented! Users can define custom rules via YAML files.

openaudit/core/rules_engine.py

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,42 @@
11
import yaml
22
import re
3+
import os
34
from pathlib import Path
4-
from typing import List
5+
from typing import List, Optional
56
from .domain import Rule
67

78
class RulesEngine:
8-
def __init__(self, rules_path: str):
9+
def __init__(self, rules_path: str, custom_rules_path: Optional[str] = None):
910
self.rules_path = Path(rules_path)
11+
self.custom_rules_path = custom_rules_path
1012

1113
def load_rules(self) -> List[Rule]:
1214
rules = []
13-
path_obj = self.rules_path
15+
16+
# Load built-in rules
17+
rules.extend(self._load_rules_from_path(self.rules_path))
18+
19+
# Load custom rules from project directory
20+
project_custom_rules = Path.cwd() / ".openaudit" / "custom_rules.yaml"
21+
if project_custom_rules.exists():
22+
rules.extend(self._load_rules_from_path(project_custom_rules))
23+
24+
# Load custom rules from home directory
25+
home_custom_rules = Path.home() / ".openaudit" / "custom_rules.yaml"
26+
if home_custom_rules.exists():
27+
rules.extend(self._load_rules_from_path(home_custom_rules))
28+
29+
# Load custom rules from provided path
30+
if self.custom_rules_path:
31+
custom_path = Path(self.custom_rules_path)
32+
if custom_path.exists():
33+
rules.extend(self._load_rules_from_path(custom_path))
34+
35+
return rules
36+
37+
def _load_rules_from_path(self, path: Path) -> List[Rule]:
38+
rules = []
39+
path_obj = Path(path)
1440

1541
if not path_obj.exists():
1642
return rules
@@ -60,5 +86,3 @@ def load_rules(self) -> List[Rule]:
6086
print(f"Error loading rules from {file_path}: {e}")
6187

6288
return rules
63-
64-
return rules

openaudit/features/debt/scanner.py

Lines changed: 44 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,17 @@
22
import re
33
import os
44
from pathlib import Path
5-
from openaudit.core.domain import Finding, Severity, Confidence, ScanContext
5+
from openaudit.core.domain import Finding, Severity, Confidence, ScanContext, Rule
66
from openaudit.core.interfaces import ScannerProtocol
77

88
class TechDebtScanner(ScannerProtocol):
99
"""
1010
Scans for technical debt, leftovers, and temporary code ("Vibe Checks").
11+
Now supports both built-in patterns and custom rules.
1112
"""
1213

13-
def __init__(self, rules: Dict = None):
14+
def __init__(self, rules: List[Rule] = None):
15+
# Built-in patterns (backward compatibility)
1416
self.patterns = {
1517
"ANNOTATION": {
1618
"pattern": re.compile(r"(TODO|FIXME|HACK|XXX|BUG):\s*(.*)", re.IGNORECASE),
@@ -33,6 +35,23 @@ def __init__(self, rules: Dict = None):
3335
"desc": "Hardcoded IP address"
3436
}
3537
}
38+
39+
# Custom rules from YAML
40+
self.custom_rules = []
41+
if rules:
42+
# Filter for technical_debt category rules
43+
self.custom_rules = [r for r in rules if r.category in ["technical_debt", "security", "general"]]
44+
self._compile_custom_rules()
45+
46+
def _compile_custom_rules(self):
47+
"""Compile regex patterns for custom rules."""
48+
self.compiled_custom_rules = []
49+
for rule in self.custom_rules:
50+
try:
51+
self.compiled_custom_rules.append((rule, re.compile(rule.regex)))
52+
except re.error as e:
53+
print(f"Warning: Failed to compile regex for rule {rule.id}: {e}")
54+
pass
3655

3756
def _iterate_files(self, context: ScanContext):
3857
target = Path(context.target_path)
@@ -79,7 +98,7 @@ def scan_content(self, content: str, file_path: str) -> List[Finding]:
7998
findings = []
8099
lines = content.splitlines()
81100

82-
# Line-by-line scanning
101+
# Scan with built-in patterns
83102
for i, line in enumerate(lines, 1):
84103
for key, rule in self.patterns.items():
85104
match = rule["pattern"].search(line)
@@ -103,6 +122,28 @@ def scan_content(self, content: str, file_path: str) -> List[Finding]:
103122
category="technical_debt",
104123
remediation="Review and clean up code."
105124
))
125+
126+
# Scan with custom rules
127+
for rule, regex in self.compiled_custom_rules:
128+
for line_num, line in enumerate(lines, 1):
129+
for match in regex.finditer(line):
130+
# Skip if ignore comment is present
131+
if "noqa" in line or "@ignore" in line:
132+
continue
133+
134+
matched_text = match.group(0).strip()[:50]
135+
findings.append(Finding(
136+
rule_id=rule.id,
137+
description=rule.description,
138+
file_path=file_path,
139+
line_number=line_num,
140+
secret_hash=matched_text,
141+
severity=rule.severity,
142+
confidence=rule.confidence,
143+
category=rule.category,
144+
remediation=rule.remediation
145+
))
146+
106147
return findings
107148

108149
return findings

openaudit/interface/cli/app.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import typer
2-
from .commands import scan_command, explain_command, config_app, mcp_command
2+
from .commands import scan_command, explain_command, config_app, mcp_command, custom_rules_app
33

44
app = typer.Typer(
55
name="OpenAuditKit",
@@ -18,4 +18,5 @@ def main_callback():
1818
app.command(name="explain")(explain_command)
1919
app.command(name="mcp")(mcp_command)
2020
app.add_typer(config_app, name="config")
21+
app.add_typer(custom_rules_app, name="custom-rules")
2122

openaudit/interface/cli/commands.py

Lines changed: 82 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,87 @@
2626
from openaudit.features.explain.agent import ExplainAgent
2727
from openaudit.interface.cli.ui import UI
2828
from openaudit.features.debt.scanner import TechDebtScanner
29+
import shutil
30+
31+
# Custom Rules Sub-app
32+
custom_rules_app = typer.Typer(name="custom-rules", help="Manage custom rules")
33+
34+
@custom_rules_app.command(name="init")
35+
def custom_rules_init(
36+
location: str = typer.Option("project", help="Where to create custom_rules.yaml: 'project' or 'home'")
37+
):
38+
"""
39+
Initialize a custom_rules.yaml template.
40+
"""
41+
if location == "home":
42+
target_dir = Path.home() / ".openaudit"
43+
target_file = target_dir / "custom_rules.yaml"
44+
else:
45+
target_dir = Path.cwd() / ".openaudit"
46+
target_file = target_dir / "custom_rules.yaml"
47+
48+
if target_file.exists():
49+
UI.warning(f"Custom rules file already exists at: {target_file}")
50+
overwrite = typer.confirm("Do you want to overwrite it?", default=False)
51+
if not overwrite:
52+
UI.info("Aborting.")
53+
raise typer.Exit(code=0)
54+
55+
# Create directory if needed
56+
target_dir.mkdir(parents=True, exist_ok=True)
57+
58+
# Copy template
59+
import openaudit
60+
package_dir = Path(openaudit.__file__).parent.parent
61+
template_file = package_dir / ".openaudit" / "custom_rules.yaml"
62+
63+
if template_file.exists():
64+
shutil.copy(template_file, target_file)
65+
UI.success(f"Custom rules template created at: {target_file}")
66+
UI.info("Edit this file to add your own rules, then run 'openaudit scan' to use them.")
67+
else:
68+
# Create a basic template inline if not found
69+
template_content = """# OpenAuditKit Custom Rules
70+
# Add your custom rules here
71+
72+
rules:
73+
- id: "CUSTOM_EXAMPLE"
74+
description: "Example custom rule"
75+
regex: "CHANGEME_[A-Z0-9]+"
76+
severity: "high"
77+
confidence: "high"
78+
category: "secret"
79+
remediation: "Replace with actual rule pattern."
80+
"""
81+
target_file.write_text(template_content, encoding='utf-8')
82+
UI.success(f"Custom rules template created at: {target_file}")
83+
UI.info("Edit this file to add your own rules.")
84+
85+
@custom_rules_app.command(name="list")
86+
def custom_rules_list():
87+
"""
88+
List all active custom rules locations.
89+
"""
90+
locations = []
91+
92+
project_custom = Path.cwd() / ".openaudit" / "custom_rules.yaml"
93+
if project_custom.exists():
94+
locations.append(("Project", str(project_custom)))
95+
96+
home_custom = Path.home() / ".openaudit" / "custom_rules.yaml"
97+
if home_custom.exists():
98+
locations.append(("Home", str(home_custom)))
99+
100+
if not locations:
101+
UI.info("No custom rules files found.")
102+
UI.info("Run 'openaudit custom-rules init' to create one.")
103+
else:
104+
UI.success("Active custom rules locations:")
105+
for label, path in locations:
106+
UI.info(f" [{label}] {path}")
107+
108+
109+
29110

30111

31112
class OutputFormat(str, Enum):
@@ -92,7 +173,7 @@ def scan_command(
92173
scanners = [
93174
SecretScanner(rules=rules),
94175
ConfigScanner(rules=rules),
95-
TechDebtScanner()
176+
TechDebtScanner(rules=rules) # Now supports custom rules
96177
]
97178

98179
# 4. Run Scan

openaudit/mcp/server.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ def scan_workspace(path: str, checks: List[str] = ["secrets", "config", "debt"])
4545
if "config" in checks:
4646
scanners.append(ConfigScanner(rules=rules))
4747
if "debt" in checks:
48-
scanners.append(TechDebtScanner())
48+
scanners.append(TechDebtScanner(rules=rules))
4949

5050
all_findings = []
5151
for scanner in scanners:
@@ -81,7 +81,7 @@ def check_file(file_path: str, content: str = None) -> str:
8181

8282
# Initialize scanners
8383
secret_scanner = SecretScanner(rules=rules)
84-
debt_scanner = TechDebtScanner()
84+
debt_scanner = TechDebtScanner(rules=rules)
8585

8686
file_content = content
8787

0 commit comments

Comments
 (0)