Pi-SEO Remediation Skill
Produce remediation cards for findings that autopr.py cannot fix automatically. Focus on Tier 2-4 fixes that require human judgment or architectural changes.
Remediation Tiers
| Tier |
Who fixes |
Type |
Examples |
| Tier 1 |
autopr.py (automated) |
Dependency updates, lint fixes |
npm audit fix, pip-audit --fix, ruff --fix |
| Tier 2 |
Developer (config change) |
Configuration, policy, env rotation |
CSP policy, CORS allowlist, rotate leaked secret, disable debug mode |
| Tier 3 |
Developer (code change) |
Code refactor or addition |
Replace eval(), parameterise SQL query, add input validation, sanitise HTML output |
| Tier 4 |
Team (architectural) |
System-level change |
Add auth middleware to entire API surface, implement rate limiting, centralise audit logging, add secrets manager |
Only produce cards for Tier 2-4. Tier 1 findings should reference autopr.py.
Remediation Card Catalogue
Cards for each of the scanner's 10 secret patterns and 10 dangerous patterns:
Secret Patterns
Anthropic API key / OpenAI API key / GitHub PAT / Linear API key / AWS access key
- Tier: 2
- Risk: Exposed credential allows full account compromise. Keys committed to git persist in history even after deletion.
- Steps:
- Immediately rotate the credential in the provider's dashboard
- Remove from source file, replace with env var reference (
os.environ["KEY_NAME"] or process.env.KEY_NAME)
- Add
.env to .gitignore if not already present
- Run
git log -S "leaked_value" --all to find all commits containing the secret
- Use
git filter-repo --path-glob '*.env' --invert-paths or BFG Repo Cleaner to purge history
- Force-push all affected branches (requires team coordination)
- Notify security team of potential exposure window
- Verification:
grep -r "old_key_prefix" . --include="*.py" --include="*.ts" returns no matches
- Effort: M
Hardcoded password / Hardcoded secret
- Tier: 2–3
- Steps:
- Generate a strong random secret:
python -c "import secrets; print(secrets.token_urlsafe(32))"
- Store in environment variable or secrets manager (Vercel env vars, Railway secrets, AWS Secrets Manager)
- Replace hardcoded value with
os.environ.get("SECRET_NAME") / process.env.SECRET_NAME
- Add validation at startup: raise if env var is absent in production
- Effort: S
Private key in source
- Tier: 2
- Steps:
- Revoke the key pair immediately
- Generate a new key pair, store private key in secrets manager only
- Remove from repo, purge git history (same BFG process as API keys)
- Effort: M
DB connection string
- Tier: 2
- Steps:
- Rotate database password
- Move connection string to
DATABASE_URL env var
- Verify ORM/driver reads from env:
os.environ["DATABASE_URL"]
- Effort: S
Dangerous Patterns
shell=True subprocess
- Tier: 3
- Risk: Command injection if any user-controlled data reaches the command string.
- Before:
subprocess.run(f"ls {user_dir}", shell=True)
- After:
subprocess.run(["ls", user_dir]) — use list form, never string interpolation
- Verification:
grep -r "shell=True" app/ --include="*.py" returns no matches
- Effort: S
eval() usage
- Tier: 3
- Risk: Arbitrary code execution if input is user-controlled.
- Before:
result = eval(user_expression)
- After: Use
ast.literal_eval() for safe data parsing, or json.loads() for JSON. If dynamic evaluation is genuinely needed, use a sandboxed interpreter.
- Effort: S–M depending on usage breadth
dangerouslySetInnerHTML
- Tier: 3
- Risk: XSS if content is not sanitised before rendering.
- Before:
<div dangerouslySetInnerHTML={{ __html: userContent }} />
- After: Sanitise with DOMPurify before passing to React:
{ __html: DOMPurify.sanitize(userContent) }
- Steps:
npm install dompurify @types/dompurify
- Import:
import DOMPurify from "dompurify"
- Wrap all user-controlled HTML through
DOMPurify.sanitize()
- Effort: S
innerHTML XSS risk
- Tier: 3
- Before:
element.innerHTML = req.body.content
- After:
element.textContent = req.body.content for plain text, or DOMPurify for HTML
- Effort: S
debug=True
- Tier: 2
- Steps:
- Replace literal
debug=True with debug=os.environ.get("DEBUG", "false").lower() == "true"
- Ensure
DEBUG is not set to true in production env vars
- Effort: S
Binding to 0.0.0.0
- Tier: 2
- Steps:
- For production: bind to
127.0.0.1 unless the service is intentionally public-facing behind a reverse proxy
- For Docker: use env var —
host = os.environ.get("HOST", "127.0.0.1")
- Confirm a reverse proxy (Nginx, Caddy, Vercel) handles public exposure
- Effort: S
TODO near sensitive keyword
- Tier: 3
- Steps:
- Review each TODO comment to determine if the security concern is still unaddressed
- Convert to a Linear ticket with proper severity classification
- Remove the TODO comment once the ticket is filed
- Effort: S
security check suppressed (# nosec)
- Tier: 2–3
- Steps:
- Read the surrounding code to understand why the suppression was added
- If suppression is justified, add a comment explaining why:
# nosec B101 — assertion only in test context
- If not justified, remove
# nosec and fix the underlying issue
- Effort: S
Output Format
{
"project_id": "string",
"generated_at": "ISO-8601",
"remediation_plan": [
{
"fingerprint": "16-char hex",
"title": "string",
"severity": "critical|high|medium|low",
"tier": 2,
"tier_label": "Config change|Code change|Architectural",
"risk_description": "string",
"steps": ["step 1", "step 2"],
"code_before": "string or null",
"code_after": "string or null",
"verification": "shell command to confirm fix",
"effort": "S|M|L",
"auto_fixable": false,
"file_path": "string",
"line_number": 0
}
]
}
1---2name: pi-seo-remediation3description: Remediation advisor for Pi-SEO findings that cannot be auto-fixed. Produces per-finding remediation cards with tier classification, step-by-step fix instructions, before/after code examples, verification commands, and effort estimates.4---56# Pi-SEO Remediation Skill78Produce remediation cards for findings that `autopr.py` cannot fix automatically. Focus on Tier 2-4 fixes that require human judgment or architectural changes.910## Remediation Tiers1112| Tier | Who fixes | Type | Examples |13|------|-----------|------|---------|14| **Tier 1** | autopr.py (automated) | Dependency updates, lint fixes | `npm audit fix`, `pip-audit --fix`, `ruff --fix` |15| **Tier 2** | Developer (config change) | Configuration, policy, env rotation | CSP policy, CORS allowlist, rotate leaked secret, disable debug mode |16| **Tier 3** | Developer (code change) | Code refactor or addition | Replace `eval()`, parameterise SQL query, add input validation, sanitise HTML output |17| **Tier 4** | Team (architectural) | System-level change | Add auth middleware to entire API surface, implement rate limiting, centralise audit logging, add secrets manager |1819Only produce cards for Tier 2-4. Tier 1 findings should reference autopr.py.2021## Remediation Card Catalogue2223Cards for each of the scanner's 10 secret patterns and 10 dangerous patterns:2425### Secret Patterns2627**Anthropic API key / OpenAI API key / GitHub PAT / Linear API key / AWS access key**28- **Tier**: 229- **Risk**: Exposed credential allows full account compromise. Keys committed to git persist in history even after deletion.30- **Steps**:31 1. Immediately rotate the credential in the provider's dashboard32 2. Remove from source file, replace with env var reference (`os.environ["KEY_NAME"]` or `process.env.KEY_NAME`)33 3. Add `.env` to `.gitignore` if not already present34 4. Run `git log -S "leaked_value" --all` to find all commits containing the secret35 5. Use `git filter-repo --path-glob '*.env' --invert-paths` or BFG Repo Cleaner to purge history36 6. Force-push all affected branches (requires team coordination)37 7. Notify security team of potential exposure window38- **Verification**: `grep -r "old_key_prefix" . --include="*.py" --include="*.ts"` returns no matches39- **Effort**: M4041**Hardcoded password / Hardcoded secret**42- **Tier**: 2–343- **Steps**:44 1. Generate a strong random secret: `python -c "import secrets; print(secrets.token_urlsafe(32))"`45 2. Store in environment variable or secrets manager (Vercel env vars, Railway secrets, AWS Secrets Manager)46 3. Replace hardcoded value with `os.environ.get("SECRET_NAME")` / `process.env.SECRET_NAME`47 4. Add validation at startup: raise if env var is absent in production48- **Effort**: S4950**Private key in source**51- **Tier**: 252- **Steps**:53 1. Revoke the key pair immediately54 2. Generate a new key pair, store private key in secrets manager only55 3. Remove from repo, purge git history (same BFG process as API keys)56- **Effort**: M5758**DB connection string**59- **Tier**: 260- **Steps**:61 1. Rotate database password62 2. Move connection string to `DATABASE_URL` env var63 3. Verify ORM/driver reads from env: `os.environ["DATABASE_URL"]`64- **Effort**: S6566### Dangerous Patterns6768**shell=True subprocess**69- **Tier**: 370- **Risk**: Command injection if any user-controlled data reaches the command string.71- **Before**: `subprocess.run(f"ls {user_dir}", shell=True)`72- **After**: `subprocess.run(["ls", user_dir])` — use list form, never string interpolation73- **Verification**: `grep -r "shell=True" app/ --include="*.py"` returns no matches74- **Effort**: S7576**eval() usage**77- **Tier**: 378- **Risk**: Arbitrary code execution if input is user-controlled.79- **Before**: `result = eval(user_expression)`80- **After**: Use `ast.literal_eval()` for safe data parsing, or `json.loads()` for JSON. If dynamic evaluation is genuinely needed, use a sandboxed interpreter.81- **Effort**: S–M depending on usage breadth8283**dangerouslySetInnerHTML**84- **Tier**: 385- **Risk**: XSS if content is not sanitised before rendering.86- **Before**: `<div dangerouslySetInnerHTML={{ __html: userContent }} />`87- **After**: Sanitise with DOMPurify before passing to React: `{ __html: DOMPurify.sanitize(userContent) }`88- **Steps**:89 1. `npm install dompurify @types/dompurify`90 2. Import: `import DOMPurify from "dompurify"`91 3. Wrap all user-controlled HTML through `DOMPurify.sanitize()`92- **Effort**: S9394**innerHTML XSS risk**95- **Tier**: 396- **Before**: `element.innerHTML = req.body.content`97- **After**: `element.textContent = req.body.content` for plain text, or DOMPurify for HTML98- **Effort**: S99100**debug=True**101- **Tier**: 2102- **Steps**:103 1. Replace literal `debug=True` with `debug=os.environ.get("DEBUG", "false").lower() == "true"`104 2. Ensure `DEBUG` is not set to `true` in production env vars105- **Effort**: S106107**Binding to 0.0.0.0**108- **Tier**: 2109- **Steps**:110 1. For production: bind to `127.0.0.1` unless the service is intentionally public-facing behind a reverse proxy111 2. For Docker: use env var — `host = os.environ.get("HOST", "127.0.0.1")`112 3. Confirm a reverse proxy (Nginx, Caddy, Vercel) handles public exposure113- **Effort**: S114115**TODO near sensitive keyword**116- **Tier**: 3117- **Steps**:118 1. Review each TODO comment to determine if the security concern is still unaddressed119 2. Convert to a Linear ticket with proper severity classification120 3. Remove the TODO comment once the ticket is filed121- **Effort**: S122123**security check suppressed (# nosec)**124- **Tier**: 2–3125- **Steps**:126 1. Read the surrounding code to understand why the suppression was added127 2. If suppression is justified, add a comment explaining why: `# nosec B101 — assertion only in test context`128 3. If not justified, remove `# nosec` and fix the underlying issue129- **Effort**: S130131## Output Format132133```json134{135 "project_id": "string",136 "generated_at": "ISO-8601",137 "remediation_plan": [138 {139 "fingerprint": "16-char hex",140 "title": "string",141 "severity": "critical|high|medium|low",142 "tier": 2,143 "tier_label": "Config change|Code change|Architectural",144 "risk_description": "string",145 "steps": ["step 1", "step 2"],146 "code_before": "string or null",147 "code_after": "string or null",148 "verification": "shell command to confirm fix",149 "effort": "S|M|L",150 "auto_fixable": false,151 "file_path": "string",152 "line_number": 0153 }154 ]155}156```