initial commit
This commit is contained in:
@@ -0,0 +1,184 @@
|
||||
---
|
||||
name: codebase-audit
|
||||
description: Systematic deep audit and bulk remediation of an existing codebase — parallel subagent review, shared utility creation, fix application, and verification. Absorbed codebase-inspection (pygount-based LOC and language breakdown).
|
||||
version: 1.0.0
|
||||
author: Hermes Agent
|
||||
license: MIT
|
||||
platforms: [linux, macos]
|
||||
metadata:
|
||||
hermes:
|
||||
tags: [code-review, audit, security, bulk-fix, remediation, xss, accessibility, quality]
|
||||
related_skills: [requesting-code-review, subagent-driven-development, systematic-debugging]
|
||||
---
|
||||
|
||||
# Codebase Audit & Bulk Remediation
|
||||
|
||||
Comprehensive error finding and fix application across a existing codebase (not a git diff).
|
||||
Scales to 30+ files using parallel subagents for review, shared utilities for fixes, and
|
||||
script-based verification.
|
||||
|
||||
**When to use:** User asks you to "check the code for errors", "find all the bugs",
|
||||
"audit the project", "make all the fixes" — any request to systematically scan an
|
||||
existing codebase (not just a diff or a single file).
|
||||
|
||||
**When NOT to use:** Pre-commit review of small changes (use `requesting-code-review`),
|
||||
single-file analysis, debugging a specific bug (use `systematic-debugging`).
|
||||
|
||||
## Phase 1 — Survey
|
||||
|
||||
Before any review, understand the codebase structure:
|
||||
|
||||
```bash
|
||||
# Get a file inventory
|
||||
find /path/to/project -type f -name '*.js' -o -name '*.html' -o -name '*.css' | sort
|
||||
|
||||
# Quick size check per file
|
||||
wc -l /path/to/project/**/*.js
|
||||
```
|
||||
|
||||
### Codebase Size & Language Breakdown (pygount, absorbed from `codebase-inspection`)
|
||||
|
||||
For a structured overview of the codebase size, language composition, and code-vs-comment ratios, use `pygount`:
|
||||
|
||||
```bash
|
||||
pip install --break-system-packages pygount 2>/dev/null || pip install pygount
|
||||
|
||||
pygount --format=summary \
|
||||
--folders-to-skip=".git,node_modules,venv,.venv,__pycache__,.cache,dist,build,.next,.tox,.eggs,*.egg-info,vendor,third_party" \
|
||||
.
|
||||
```
|
||||
|
||||
**Always use `--folders-to-skip`** — without it, pygount crawls node_modules and can hang.
|
||||
|
||||
**Language-specific filtering:**
|
||||
```bash
|
||||
# Only count Python files
|
||||
pygount --suffix=py --format=summary .
|
||||
```
|
||||
|
||||
**Output columns:** Language, Files, Code lines, Comment lines, % of total.
|
||||
|
||||
**Pseudo-languages to expect:** `__empty__`, `__binary__`, `__generated__`, `__duplicate__`, `__unknown__`.
|
||||
|
||||
**Pitfall:** Markdown content is classified as comments (0 code lines). This is expected.
|
||||
|
||||
Identify the stack, key files, and group files into review batches of 5-8 files each.
|
||||
Keep related files together (e.g., all shared/ files in one batch).
|
||||
|
||||
## Phase 2 — Parallel Subagent Review
|
||||
|
||||
Use `delegate_task(tasks=[...])` with up to max_concurrent_children subagents.
|
||||
Each subagent gets a batch of files and a clear review mandate.
|
||||
|
||||
**Per-task structure:**
|
||||
```python
|
||||
{
|
||||
"goal": "Review these files for errors, bugs, security issues, and code quality problems. Report everything you find.",
|
||||
"context": "File paths, what each file does, what to prioritize",
|
||||
"toolsets": ["file"]
|
||||
}
|
||||
```
|
||||
|
||||
**Review categories to check every file for:**
|
||||
|
||||
- **🔴 Critical (will crash):** ReferenceError on undefined variables, TypeError on null/undefined access (.toDate() on null, .toUpperCase() on undefined), malformed API URLs, variable shadowing in closures
|
||||
- **🟠 Security:** XSS via innerHTML with unsanitized user/Firestore data, inline onclick handlers with string interpolation (`onclick="fn('${data}')"`), client-side API keys, eval()-like patterns, subresource integrity (SRI) missing on CDN scripts, Content-Security-Policy absent
|
||||
- **🟡 Logic bugs:** Infinite retries with no backoff, memory leaks (accumulating event listeners on re-render, timer nuking that clears ALL browser timers), duplicate field names in data models, missing null guards on optional fields
|
||||
- **🟢 Accessibility:** Missing role="dialog"/aria-modal on modals, missing aria-live on dynamic content, missing aria-label on icon-only buttons, no focus trapping in modals, no skip-to-content link
|
||||
- **⚪ Quality:** Dead code (empty files, unreachable branches), excessive console.log, redundant duplicate code, hardcoded magic numbers
|
||||
|
||||
## Phase 3 — Prioritize and Plan Fixes
|
||||
|
||||
Aggregate all findings from subagents, group by severity, and plan the fix order:
|
||||
|
||||
1. **Critical crashes** first — these break the app at runtime
|
||||
2. **Security vulnerabilities** — XSS, exposed keys, CSP/SRI
|
||||
3. **Logic bugs** — retries, memory leaks, data integrity
|
||||
4. **Accessibility** — screen reader support
|
||||
5. **Code quality** — cleanup
|
||||
|
||||
## Phase 4 — Create Shared Utilities
|
||||
|
||||
Before fixing individual files, create any shared helpers the fixes need:
|
||||
|
||||
```javascript
|
||||
// shared/sanitize.js — XSS prevention
|
||||
export function escapeHtml(value) {
|
||||
if (value == null) return '';
|
||||
const str = String(value);
|
||||
const div = document.createElement('div');
|
||||
div.appendChild(document.createTextNode(str));
|
||||
return div.innerHTML;
|
||||
}
|
||||
```
|
||||
|
||||
Create this FIRST so all fix subagents can import it.
|
||||
|
||||
## Phase 5 — Apply Fixes in Parallel
|
||||
|
||||
Use `delegate_task(tasks=[...])` again, this time for fixes. Group related fixes:
|
||||
|
||||
**Batch structure (common groupings):**
|
||||
- **Bug-fix batch:** 1 subagent per large file (dashboard.js, repair-orders.js)
|
||||
- **XSS batch:** 1 subagent to add escapeHtml imports + wrap all interpolations across 5-8 files
|
||||
- **HTML/a11y batch:** index.html and all other HTML pages
|
||||
- **Config batch:** firebase.json, etc.
|
||||
|
||||
**Fix patterns to apply:**
|
||||
```javascript
|
||||
// Before (XSS via innerHTML):
|
||||
element.innerHTML = `<div>${userData}</div>`;
|
||||
|
||||
// After:
|
||||
element.innerHTML = `<div>${escapeHtml(userData)}</div>`;
|
||||
|
||||
// Before (null crash):
|
||||
writeupTime: doc.data().writeupTime.toDate()
|
||||
|
||||
// After (optional chaining):
|
||||
writeupTime: doc.data().writeupTime?.toDate?.() ?? new Date()
|
||||
|
||||
// Before (infinite retry):
|
||||
setTimeout(() => loadData(), 3000);
|
||||
|
||||
// After (bounded retry):
|
||||
let retryCount = 0;
|
||||
function retry(fn, max = 3, delay = 3000) {
|
||||
retryCount++;
|
||||
if (retryCount > max) return;
|
||||
setTimeout(fn, delay * Math.pow(2, retryCount - 1));
|
||||
}
|
||||
```
|
||||
|
||||
**Per-fix-subagent instructions must be SPECIFIC** — list exact line ranges or
|
||||
search patterns, exact old_string → new_string, and how to find them
|
||||
(e.g., "search for `onclick=\"...\${...}` patterns in repair-orders.js").
|
||||
|
||||
## Phase 6 — Verification
|
||||
|
||||
After all fixes, run a grep-based verification script to confirm each fix was applied:
|
||||
|
||||
```python
|
||||
from hermes_tools import terminal
|
||||
|
||||
checks = [
|
||||
('file.js', 'pattern_to_confirm'),
|
||||
('file.js', 'escapeHtml'), # check XSS fix applied
|
||||
]
|
||||
|
||||
for file, pat in checks:
|
||||
r = terminal(f"grep -c '{pat}' '{path}' 2>/dev/null || echo '0'")
|
||||
count = r['output'].strip().split('\n')[0]
|
||||
ok = count != '0'
|
||||
print(f"{'✅' if ok else '❌'} {file}: '{pat}' -> {count}")
|
||||
```
|
||||
|
||||
## Pitfalls
|
||||
|
||||
- **Subagents exceed tool iteration limits** — big files (3000+ lines) may hit 50-iteration cap. Split fixes across multiple subagents or fix smaller batches at a time.
|
||||
- **Patch tool finds multiple matches** — when a pattern appears in two similar functions (e.g., both `getPriorityAnalysis` and `handleAiWrite`), use `replace_all=True` or add more context lines to disambiguate.
|
||||
- **Re-read warnings** — subagents that modify files you previously read will trigger "file was last read with partial view" warnings. Re-read the file before further patches.
|
||||
- **XSS is pervasive** — in a ~30 file codebase, expect 80-120+ interpolation points. Don't try to fix them all in one subagent task; batch by file group.
|
||||
- **Timer nuking** is a common anti-pattern in prototyped apps — watch for `for (let i = 0; i < highestTimeoutId; i++) clearTimeout(i)` patterns.
|
||||
- **delegate_task max_concurrent_children** defaults to 3. If you need more parallelism, mention it to the user.
|
||||
- **Don't fix what you can't verify** — API keys marked `***` can't be fixed to real values; mark them as TODO. Empty cloud functions stubs with no exports are intentional blanks.
|
||||
@@ -0,0 +1,187 @@
|
||||
# Common Web App Bugs Found in Audits
|
||||
|
||||
## XSS via innerHTML Template Literals
|
||||
|
||||
**Pattern:** `${userData}` inside backtick strings assigned to `.innerHTML`
|
||||
|
||||
**Files affected:** customers.js, appointments.js, repair-orders.js, quote-tab-manager.js, financial-dashboard.js, invoice-manager.js, dashboard.js
|
||||
|
||||
**Fix:** Import escapeHtml from a shared sanitize module and wrap every user/Firestore-derived value:
|
||||
|
||||
```javascript
|
||||
import { escapeHtml } from './shared/sanitize.js';
|
||||
|
||||
// Before:
|
||||
`<p>${customer.name}</p>`
|
||||
// After:
|
||||
`<p>${escapeHtml(customer.name)}</p>`
|
||||
```
|
||||
|
||||
**Watch for:** Template literals inside `.map().join('')` chains that build HTML tables/lists, data attribute injectors like `data-customer='${JSON.stringify(obj)}'`, and numeric fields that could still have injection via toString().
|
||||
|
||||
## Stored XSS via Inline onclick
|
||||
|
||||
**Pattern:** `onclick="fn('${userData}')"` in HTML template literals
|
||||
|
||||
**Fix:** Remove inline onclick, use addEventListener with data-* attributes:
|
||||
|
||||
```javascript
|
||||
// Before:
|
||||
`<button onclick="openModal('${ro.id}', '${ro.status}')">Edit</button>`
|
||||
|
||||
// After:
|
||||
`<button class="edit-btn" data-ro-id="${escapeHtml(ro.id)}" data-status="${escapeHtml(ro.status)}">Edit</button>`
|
||||
// In JS:
|
||||
document.querySelectorAll('.edit-btn').forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
openModal(btn.dataset.roId, btn.dataset.status);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## Null Crash on Firestore Timestamp Fields
|
||||
|
||||
**Pattern:** `doc.data().timestampField.toDate()` without null check
|
||||
|
||||
**Fix:** Optional chaining with fallback:
|
||||
|
||||
```javascript
|
||||
// Before:
|
||||
writeupTime: doc.data().writeupTime.toDate(),
|
||||
promisedTime: doc.data().promisedTime.toDate(),
|
||||
|
||||
// After:
|
||||
writeupTime: doc.data().writeupTime?.toDate?.() ?? new Date(),
|
||||
promisedTime: doc.data().promisedTime?.toDate?.() ?? new Date()
|
||||
```
|
||||
|
||||
Also check `.toUpperCase()`, `.toLocaleString()`, and `.toDateString()` calls on fields that might be undefined.
|
||||
|
||||
## Timer Nuking
|
||||
|
||||
**Pattern:** Aggressive loop clearing ALL browser timers:
|
||||
|
||||
```javascript
|
||||
const highestTimeoutId = setTimeout(() => {}, 0);
|
||||
for (let i = 0; i < highestTimeoutId; i++) {
|
||||
clearTimeout(i);
|
||||
}
|
||||
```
|
||||
|
||||
**Fix:** Track your own timeouts:
|
||||
|
||||
```javascript
|
||||
let activeTimeouts = [];
|
||||
let activeIntervals = [];
|
||||
|
||||
function trackedSetTimeout(fn, ms) {
|
||||
const id = setTimeout(() => {
|
||||
activeTimeouts = activeTimeouts.filter(t => t !== id);
|
||||
fn();
|
||||
}, ms);
|
||||
activeTimeouts.push(id);
|
||||
return id;
|
||||
}
|
||||
|
||||
function clearNamedTimers() {
|
||||
for (const id of activeTimeouts) clearTimeout(id);
|
||||
for (const id of activeIntervals) clearInterval(id);
|
||||
activeTimeouts = [];
|
||||
activeIntervals = [];
|
||||
}
|
||||
```
|
||||
|
||||
## Infinite Retry With No Backoff
|
||||
|
||||
**Pattern:** `setTimeout(() => loadData(), 3000)` in catch block with no retry limit
|
||||
|
||||
**Fix:** Bounded retry with exponential backoff:
|
||||
|
||||
```javascript
|
||||
let retryCount = 0;
|
||||
const MAX_RETRIES = 3;
|
||||
|
||||
// Inside catch block:
|
||||
retryCount++;
|
||||
if (retryCount <= MAX_RETRIES) {
|
||||
const delay = Math.min(3000 * Math.pow(2, retryCount - 1), 30000);
|
||||
setTimeout(() => loadData(), delay);
|
||||
} else {
|
||||
showNotification('Failed after multiple attempts. Please refresh.', true);
|
||||
}
|
||||
```
|
||||
|
||||
## Duplicate Variable in Closure (Shadowing Bug)
|
||||
|
||||
**Pattern:** A variable name used both at function scope and inside an `if` block:
|
||||
|
||||
```javascript
|
||||
const roIndex = repairOrders.findIndex(ro => ro.id === roId);
|
||||
if (roIndexStr !== -1) {
|
||||
const roIndex = roIndexStr; // SHADOWS outer roIndex!
|
||||
repairOrders.splice(roIndex, 1);
|
||||
}
|
||||
// Outside if: roIndex is still -1 (original value)
|
||||
let actualIndex = roIndex; // BUG: uses -1
|
||||
```
|
||||
|
||||
**Fix:** Rename the inner variable or use a single variable throughout:
|
||||
|
||||
```javascript
|
||||
let roArrayIndex = repairOrders.findIndex(ro => ro.id === roId);
|
||||
if (roArrayIndex === -1) {
|
||||
roArrayIndex = repairOrders.findIndex(ro => ro.roNumber === roId);
|
||||
}
|
||||
```
|
||||
|
||||
## Accumulating Global Click Listeners
|
||||
|
||||
**Pattern:** `document.addEventListener('click', ...)` inside a function called on every render:
|
||||
|
||||
```javascript
|
||||
function renderList(items) {
|
||||
container.innerHTML = items.map(createCard).join('');
|
||||
document.addEventListener('click', handleCardClick); // NEW LISTENER EVERY RENDER!
|
||||
}
|
||||
```
|
||||
|
||||
**Fix:** Use a flag or event delegation on the container instead:
|
||||
|
||||
```javascript
|
||||
let listenersAttached = false;
|
||||
|
||||
function renderList(items) {
|
||||
container.innerHTML = items.map(createCard).join('');
|
||||
if (!listenersAttached) {
|
||||
container.addEventListener('click', handleCardClick);
|
||||
listenersAttached = true;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Or better, delegate on the container and let event bubbling handle dynamically added children without re-attaching.
|
||||
|
||||
## Cache-Control Locking Users Out of Updates
|
||||
|
||||
**Pattern:** `firebase.json` with `max-age=31536000` (1 year) on JS/CSS
|
||||
|
||||
**Fix:** Significantly reduce cache time and add `must-revalidate`:
|
||||
|
||||
```json
|
||||
{
|
||||
"source": "**/*.@(js|css)",
|
||||
"headers": [{
|
||||
"key": "Cache-Control",
|
||||
"value": "max-age=3600, must-revalidate"
|
||||
}]
|
||||
},
|
||||
{
|
||||
"source": "**/*.html",
|
||||
"headers": [{
|
||||
"key": "Cache-Control",
|
||||
"value": "no-cache, must-revalidate"
|
||||
}]
|
||||
}
|
||||
```
|
||||
|
||||
For production, use content fingerprinting in filenames (e.g., `app.a1b2c3.js`) so you can safely use long cache durations without staleness.
|
||||
Reference in New Issue
Block a user