Skip to content

Commit 48cdc23

Browse files
committed
Add pre-commit hook and in-memory scanning support
Introduced a .pre-commit-config.yaml for pre-commit integration and added 'pre-commit' as a dev dependency. Enhanced SecretScanner and TechDebtScanner with scan_content methods for in-memory scanning, and updated MCP's check_file to support scanning file content directly without requiring disk writes. Updated documentation to reflect these changes and mark related roadmap items as complete.
1 parent b962001 commit 48cdc23

7 files changed

Lines changed: 139 additions & 76 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,3 +170,4 @@ memory_bank.md
170170
openaudit.egg-info/PKG-INFO
171171
openaudit.egg-info/requires.txt
172172
/openaudit.egg-info
173+
memory_bank.md

.pre-commit-config.yaml

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
repos:
2+
- repo: https://github.com/pre-commit/pre-commit-hooks
3+
rev: v4.5.0
4+
hooks:
5+
- id: trailing-whitespace
6+
- id: end-of-file-fixer
7+
- id: check-yaml
8+
- id: check-added-large-files
9+
10+
- repo: local
11+
hooks:
12+
- id: openaudit-scan
13+
name: OpenAuditKit Scan
14+
entry: openaudit scan --fail-on high
15+
language: system
16+
types: [python]
17+
pass_filenames: false
18+
description: "Runs OpenAuditKit scan on the repository before commit."

README.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,22 @@ Turn your favorite AI Editor (Cursor, Claude Desktop) into a security expert. Op
9595

9696
The AI will invoke OpenAuditKit locally and report back findings instantly!
9797

98+
## 🪝 Git Hooks (Pre-commit)
99+
100+
Prevent security issues from being committed by using our pre-configured git hooks.
101+
102+
1. **Install pre-commit**:
103+
```bash
104+
pip install pre-commit
105+
```
106+
107+
2. **Install the hooks**:
108+
```bash
109+
pre-commit install
110+
```
111+
112+
Now, `openaudit` will automatically scan your code before every commit!
113+
98114
## 🛡️ VibeGuard (Tech Debt Scanner)
99115

100116
We know "vibecodin" happens. OpenAuditKit now includes **VibeGuard**, a scanner designed to catch non-engineering practices before they hit production:

openaudit/features/debt/scanner.py

Lines changed: 34 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -68,34 +68,41 @@ def scan(self, context: ScanContext) -> List[Finding]:
6868
for file_path in self._iterate_files(context):
6969
try:
7070
content = Path(file_path).read_text(encoding="utf-8", errors="ignore")
71-
lines = content.splitlines()
72-
73-
# Line-by-line scanning
74-
for i, line in enumerate(lines, 1):
75-
for key, rule in self.patterns.items():
76-
match = rule["pattern"].search(line)
77-
if match:
78-
# Skip if specific ignore comment is present
79-
if "noqa" in line or "@ignore" in line:
80-
continue
81-
82-
detail = match.group(0).strip()[:50] # truncated
83-
if key == "ANNOTATION":
84-
detail = match.group(2).strip() or "Untitled Task"
85-
86-
findings.append(Finding(
87-
rule_id=f"DEBT-{key}",
88-
description=f"{rule['desc']}: {detail}",
89-
file_path=str(Path(file_path).relative_to(context.target_path)),
90-
line_number=i,
91-
secret_hash=match.group(0).strip()[:20],
92-
severity=rule["severity"],
93-
confidence=Confidence.HIGH,
94-
category="technical_debt",
95-
remediation="Review and clean up code."
96-
))
97-
71+
# Calculate relative path for reporting
72+
rel_path = str(Path(file_path).relative_to(context.target_path))
73+
findings.extend(self.scan_content(content, rel_path))
9874
except Exception:
9975
continue
76+
return findings
77+
78+
def scan_content(self, content: str, file_path: str) -> List[Finding]:
79+
findings = []
80+
lines = content.splitlines()
81+
82+
# Line-by-line scanning
83+
for i, line in enumerate(lines, 1):
84+
for key, rule in self.patterns.items():
85+
match = rule["pattern"].search(line)
86+
if match:
87+
# Skip if specific ignore comment is present
88+
if "noqa" in line or "@ignore" in line:
89+
continue
90+
91+
detail = match.group(0).strip()[:50] # truncated
92+
if key == "ANNOTATION":
93+
detail = match.group(2).strip() or "Untitled Task"
94+
95+
findings.append(Finding(
96+
rule_id=f"DEBT-{key}",
97+
description=f"{rule['desc']}: {detail}",
98+
file_path=file_path,
99+
line_number=i,
100+
secret_hash=match.group(0).strip()[:20],
101+
severity=rule["severity"],
102+
confidence=Confidence.HIGH,
103+
category="technical_debt",
104+
remediation="Review and clean up code."
105+
))
106+
return findings
100107

101108
return findings

openaudit/features/secrets/scanner.py

Lines changed: 27 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -60,35 +60,39 @@ def _scan_file(self, file_path: Path) -> List[Finding]:
6060
return findings
6161

6262
content = file_path.read_text(encoding='utf-8', errors='ignore')
63-
64-
for rule, regex in self.compiled_rules:
65-
for line_num, line in enumerate(content.splitlines(), start=1):
66-
for match in regex.finditer(line):
67-
if match.lastindex and match.lastindex >= 1:
68-
secret_candidate = match.group(1)
69-
else:
70-
secret_candidate = match.group(0)
71-
72-
if rule.entropy_check:
73-
if self._calculate_entropy(secret_candidate) < 4.5: # Threshold
74-
continue
75-
76-
findings.append(Finding(
77-
rule_id=rule.id,
78-
description=rule.description,
79-
file_path=str(file_path),
80-
line_number=line_num,
81-
secret_hash=self._mask_secret(secret_candidate),
82-
severity=rule.severity,
83-
confidence=rule.confidence,
84-
category="secret"
85-
))
63+
return self.scan_content(content, str(file_path))
8664
except Exception as e:
8765
# log or ignore
8866
pass
8967

9068
return findings
9169

70+
def scan_content(self, content: str, file_path: str) -> List[Finding]:
71+
findings = []
72+
for rule, regex in self.compiled_rules:
73+
for line_num, line in enumerate(content.splitlines(), start=1):
74+
for match in regex.finditer(line):
75+
if match.lastindex and match.lastindex >= 1:
76+
secret_candidate = match.group(1)
77+
else:
78+
secret_candidate = match.group(0)
79+
80+
if rule.entropy_check:
81+
if self._calculate_entropy(secret_candidate) < 4.5: # Threshold
82+
continue
83+
84+
findings.append(Finding(
85+
rule_id=rule.id,
86+
description=rule.description,
87+
file_path=file_path,
88+
line_number=line_num,
89+
secret_hash=self._mask_secret(secret_candidate),
90+
severity=rule.severity,
91+
confidence=rule.confidence,
92+
category="secret"
93+
))
94+
return findings
95+
9296
def _calculate_entropy(self, data: str) -> float:
9397
if not data:
9498
return 0

openaudit/mcp/server.py

Lines changed: 41 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -67,37 +67,53 @@ def scan_workspace(path: str, checks: List[str] = ["secrets", "config", "debt"])
6767
return report
6868

6969
@mcp.tool()
70-
def check_file(file_path: str, content: str) -> str:
70+
def check_file(file_path: str, content: str = None) -> str:
7171
"""
7272
Checks a single file content for security issues immediately (useful for pre-save checks).
73-
This function creates a temporary file to run the scan if needed, or uses regex directly
74-
depending on scanner implementation. For consistency, we'll try to scan the file path
75-
assuming it's on disk, or warn if it requires disk presence.
76-
"""
77-
# For now, we assume the file exists on disk at file_path since scanners read from disk.
78-
# In a real "vibecoding" stream, we might want to scan 'content' directly.
79-
# OpenAudit scanners currently take a Context which iterates files.
80-
# We will fallback to calling scan_workspace on the parent folder but filtering for this file
81-
# Or simplified: Just reuse scan_workspace logic but strictly for one file.
73+
If 'content' is provided, it scans that content directly without reading from disk.
74+
If 'content' is NOT provided, it reads the file from disk.
8275
83-
path_obj = Path(file_path).absolute()
84-
if not path_obj.exists():
85-
# Note: We currently require file to exist on disk.
86-
# Future improvement: Support verifying in-memory content.
87-
return f"Error: File {file_path} must exist on disk to be scanned."
88-
76+
Args:
77+
file_path: The path of the file (used for reporting and rule matching).
78+
content: Optional. The actual content of the file.
79+
"""
8980
rules = _get_rules()
90-
ignore_manager = IgnoreManager(root_path=path_obj.parent)
91-
context = ScanContext(target_path=str(path_obj.parent), ignore_manager=ignore_manager)
9281

93-
# We manually override context iteration or just initialize scanners that handle single files well.
94-
# SecretScanner detects file vs dir.
82+
# Initialize scanners
83+
secret_scanner = SecretScanner(rules=rules)
84+
debt_scanner = TechDebtScanner()
85+
86+
file_content = content
87+
88+
# If content is not provided, try to read from disk
89+
if file_content is None:
90+
path_obj = Path(file_path).absolute()
91+
if not path_obj.exists():
92+
return f"Error: File {file_path} does not exist on disk and no content provided."
93+
try:
94+
file_content = path_obj.read_text(encoding='utf-8', errors='ignore')
95+
except Exception as e:
96+
return f"Error reading file: {str(e)}"
97+
98+
all_findings = []
99+
100+
# Run scans on content
101+
all_findings.extend(secret_scanner.scan_content(file_content, file_path))
102+
all_findings.extend(debt_scanner.scan_content(file_content, file_path))
95103

96-
# Let's create a tailored context scan
97-
scanners = [
98-
SecretScanner(rules=rules),
99-
TechDebtScanner() # Code quality is important for single files too
100-
]
104+
if not all_findings:
105+
return "✅ OpenAuditKit: File looks clean!"
106+
107+
# Format output
108+
report = f"🚨 Issues Found in {file_path}:\n\n"
109+
for f in all_findings:
110+
report += f"- [{f.severity.value.upper()}] {f.category.upper()} at line {f.line_number}\n"
111+
report += f" Details: {f.description}\n"
112+
if f.remediation:
113+
report += f" Fix: {f.remediation}\n"
114+
report += "\n"
115+
116+
return report
101117

102118
# The scanners expect context.target_path to be the root.
103119
# Whatever, let's just use the SecretScanner internal _scan_file if we could exposed it,

pyproject.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,8 @@ dependencies = [
3131
[project.optional-dependencies]
3232
dev = [
3333
"pytest>=7.0.0",
34-
"pytest-cov>=4.0.0"
34+
"pytest-cov>=4.0.0",
35+
"pre-commit>=3.5.0"
3536
]
3637

3738
[project.scripts]

0 commit comments

Comments
 (0)