Clean up nested duplicate: v2/v2/skills/security_review.py
Browse files- v2/v2/skills/security_review.py +0 -249
v2/v2/skills/security_review.py
DELETED
|
@@ -1,249 +0,0 @@
|
|
| 1 |
-
"""Security review chip + the audit findings as structured data.
|
| 2 |
-
|
| 3 |
-
AUDIT_FINDINGS is the single source of truth for the 13 confirmed
|
| 4 |
-
findings from the Nexus audit (2026-07-10, adversarially red-teamed,
|
| 5 |
-
43% survival rate). engine/beliefs.py seeds BDI beliefs from this list;
|
| 6 |
-
the discipline gate checks drafts against the same list. One place to
|
| 7 |
-
update when the campus team fixes a finding.
|
| 8 |
-
"""
|
| 9 |
-
|
| 10 |
-
import re
|
| 11 |
-
from dataclasses import dataclass, field
|
| 12 |
-
|
| 13 |
-
from kintsugi_core import (
|
| 14 |
-
BaseSkillChip,
|
| 15 |
-
EFEWeights,
|
| 16 |
-
SkillCapability,
|
| 17 |
-
SkillContext,
|
| 18 |
-
SkillDomain,
|
| 19 |
-
SkillRequest,
|
| 20 |
-
SkillResponse,
|
| 21 |
-
)
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
@dataclass(frozen=True)
|
| 25 |
-
class AuditFinding:
|
| 26 |
-
id: str
|
| 27 |
-
severity: str # CRITICAL | HIGH | MEDIUM | LOW
|
| 28 |
-
title: str
|
| 29 |
-
area: str # auth | webhook | transaction | realtime | schema | moderation | cost
|
| 30 |
-
file_hint: str
|
| 31 |
-
advice: str
|
| 32 |
-
patterns: tuple = () # regexes that indicate the same mistake recurring
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
AUDIT_FINDINGS = [
|
| 36 |
-
AuditFinding(
|
| 37 |
-
id="C1", severity="CRITICAL", area="schema",
|
| 38 |
-
title="Lecture instructor_id type mismatch — all lectures broken",
|
| 39 |
-
file_hint="server/src/services/lectureCapture.ts:48",
|
| 40 |
-
advice=("socket.userId is a numeric string but lecture_sessions."
|
| 41 |
-
"instructor_id is a UUID column. Use consistent ID types; "
|
| 42 |
-
"never compare socket IDs to DB UUIDs with ===."),
|
| 43 |
-
patterns=(r"socket\.userId\s*===", r"instructor_id"),
|
| 44 |
-
),
|
| 45 |
-
AuditFinding(
|
| 46 |
-
id="C2", severity="CRITICAL", area="webhook",
|
| 47 |
-
title="Webhook signature bypass when env var is unset",
|
| 48 |
-
file_hint="server/src/routes/webhook.ts:219",
|
| 49 |
-
advice=("Never skip signature verification when a secret env var "
|
| 50 |
-
"is missing — reject the request instead, or require "
|
| 51 |
-
"NODE_ENV=development for any bypass."),
|
| 52 |
-
patterns=(r"if\s*\(\s*!\s*secret\s*\)\s*return\s+true",
|
| 53 |
-
r"WEBHOOK_SECRET\s*\|\|\s*['\"]['\"]"),
|
| 54 |
-
),
|
| 55 |
-
AuditFinding(
|
| 56 |
-
id="H1", severity="HIGH", area="auth",
|
| 57 |
-
title="JWT_SECRET falls back to empty string",
|
| 58 |
-
file_hint="server/src/middleware/auth.ts:12",
|
| 59 |
-
advice=("Startup must be fatal (process.exit(1)) when JWT_SECRET "
|
| 60 |
-
"is empty or a placeholder. Never sign or verify with a "
|
| 61 |
-
"defaulted secret."),
|
| 62 |
-
patterns=(r"JWT_SECRET\s*(\?\?|\|\|)\s*['\"]",),
|
| 63 |
-
),
|
| 64 |
-
AuditFinding(
|
| 65 |
-
id="H2", severity="HIGH", area="realtime",
|
| 66 |
-
title="connect_error triggers rapid reconnection loop",
|
| 67 |
-
file_hint="client/src/stores/presenceStore.ts:578",
|
| 68 |
-
advice=("All reconnect paths need exponential backoff. Only "
|
| 69 |
-
"refresh tokens on 401/403, not on every connect_error."),
|
| 70 |
-
patterns=(r"connect_error", r"refreshToken\(\)\s*.*connect\(\)"),
|
| 71 |
-
),
|
| 72 |
-
AuditFinding(
|
| 73 |
-
id="H3", severity="HIGH", area="transaction",
|
| 74 |
-
title="Gem debit without transaction protection",
|
| 75 |
-
file_hint="server/src/routes/store.ts:1108",
|
| 76 |
-
advice=("debitGems/creditGems plus any dependent operation must "
|
| 77 |
-
"share one DB transaction. A crash between them loses "
|
| 78 |
-
"currency permanently."),
|
| 79 |
-
patterns=(r"debitGems\(", r"creditGems\("),
|
| 80 |
-
),
|
| 81 |
-
AuditFinding(
|
| 82 |
-
id="H4", severity="HIGH", area="realtime",
|
| 83 |
-
title="Trade system has no accept handler — feature incomplete",
|
| 84 |
-
file_hint="server/src/services/socketHandlers/tradeHandlers.ts",
|
| 85 |
-
advice=("trade:create-offer exists but accept/decline/cancel do "
|
| 86 |
-
"not. Don't build on the trade flow assuming it completes."),
|
| 87 |
-
patterns=(r"trade:(accept|create)-offer",),
|
| 88 |
-
),
|
| 89 |
-
AuditFinding(
|
| 90 |
-
id="M1", severity="MEDIUM", area="auth",
|
| 91 |
-
title="JWT_SECRET prefix leaked into NPC Matrix password",
|
| 92 |
-
file_hint="server/src/services/shopkeeperTools.ts:368",
|
| 93 |
-
advice=("Never derive user-visible values from signing secrets. "
|
| 94 |
-
"Use a separate secret or an HMAC derivation."),
|
| 95 |
-
patterns=(r"JWT_SECRET\?*\.slice\(",),
|
| 96 |
-
),
|
| 97 |
-
AuditFinding(
|
| 98 |
-
id="M2", severity="MEDIUM", area="auth",
|
| 99 |
-
title="Matrix admins receive isAdmin=true in client response",
|
| 100 |
-
file_hint="server/src/routes/auth.ts:87",
|
| 101 |
-
advice=("Client-facing admin flags must reflect server-enforced "
|
| 102 |
-
"roles only; Matrix server admin is not campus admin."),
|
| 103 |
-
patterns=(r"isMatrixServerAdmin", r"isAdmin\s*[:=]\s*true"),
|
| 104 |
-
),
|
| 105 |
-
AuditFinding(
|
| 106 |
-
id="M3", severity="MEDIUM", area="auth",
|
| 107 |
-
title="No security headers (helmet/CSP/HSTS missing)",
|
| 108 |
-
file_hint="server/src/index.ts",
|
| 109 |
-
advice="Add helmet middleware with an appropriate CSP.",
|
| 110 |
-
patterns=(r"app\.use\(helmet",),
|
| 111 |
-
),
|
| 112 |
-
AuditFinding(
|
| 113 |
-
id="M4", severity="MEDIUM", area="moderation",
|
| 114 |
-
title="Faculty can kick/ban other faculty and admins",
|
| 115 |
-
file_hint="server/src/services/socketHandlers/facultyPanelHandlers.ts:259",
|
| 116 |
-
advice=("Moderation handlers must check the TARGET's role, not "
|
| 117 |
-
"just the actor's — no acting on equal/higher privilege."),
|
| 118 |
-
patterns=(r"(kick|ban|timeout).*(faculty|moderator)",),
|
| 119 |
-
),
|
| 120 |
-
AuditFinding(
|
| 121 |
-
id="M5", severity="MEDIUM", area="cost",
|
| 122 |
-
title="No per-student message cap on agent conversations",
|
| 123 |
-
file_hint="server/src/services/socketHandlers/agentHandlers.ts",
|
| 124 |
-
advice=("Every new LLM-calling path needs a per-student daily cap "
|
| 125 |
-
"or token budget."),
|
| 126 |
-
patterns=(r"npc:message", r"callLLM\("),
|
| 127 |
-
),
|
| 128 |
-
AuditFinding(
|
| 129 |
-
id="L1", severity="LOW", area="schema",
|
| 130 |
-
title="Foreign keys without ON DELETE break agent deletion",
|
| 131 |
-
file_hint="migrations 193/223/303",
|
| 132 |
-
advice=("New FKs referencing agents (or similar parents) need "
|
| 133 |
-
"ON DELETE CASCADE or explicit dependent cleanup."),
|
| 134 |
-
patterns=(r"REFERENCES\s+\w+\s*\([^)]*\)\s*(?!.*ON DELETE)",),
|
| 135 |
-
),
|
| 136 |
-
AuditFinding(
|
| 137 |
-
id="L2", severity="LOW", area="transaction",
|
| 138 |
-
title="Agent job payments without transaction",
|
| 139 |
-
file_hint="server/src/services/agentAutonomy.ts:1704",
|
| 140 |
-
advice=("gem_balance and earnings updates must share one "
|
| 141 |
-
"transaction."),
|
| 142 |
-
patterns=(r"gem_balance", r"total_gems_earned"),
|
| 143 |
-
),
|
| 144 |
-
]
|
| 145 |
-
|
| 146 |
-
# Recurring anti-patterns the audit told the team to grep for.
|
| 147 |
-
RECURRING_PATTERNS = [
|
| 148 |
-
("env_fallback_disables_security",
|
| 149 |
-
r"(SECRET|_KEY|TOKEN)\w*\s*(\?\?|\|\|)\s*['\"]",
|
| 150 |
-
"Env var fallback that silently degrades security (audit pattern 4)."),
|
| 151 |
-
("parseint_no_nan_guard",
|
| 152 |
-
r"parseInt\(req\.params",
|
| 153 |
-
"parseInt on route params without a NaN guard (audit pattern 3)."),
|
| 154 |
-
("socket_on_without_off",
|
| 155 |
-
r"socket\.on\(",
|
| 156 |
-
"Socket listener registration — confirm matching socket.off cleanup "
|
| 157 |
-
"(audit pattern 2)."),
|
| 158 |
-
]
|
| 159 |
-
|
| 160 |
-
AUTH_SIGNALS = [
|
| 161 |
-
r"\bJWT_SECRET\b", r"\bverifyToken\b", r"\brequireAdmin\b",
|
| 162 |
-
r"\brequireModerator\b", r"\bauthMiddleware\b", r"\bsession\s*cookie\b",
|
| 163 |
-
r"\brejectIfIneligible\b", r"\bBearer\b", r"\brefresh[_ ]?token\b",
|
| 164 |
-
r"\bwebhook\b", r"\bsignature\b",
|
| 165 |
-
]
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
def findings_relevant_to(text: str) -> list:
|
| 169 |
-
"""Findings whose area keywords or patterns appear in the text."""
|
| 170 |
-
hits = []
|
| 171 |
-
lower = text.lower()
|
| 172 |
-
for f in AUDIT_FINDINGS:
|
| 173 |
-
matched = any(re.search(p, text, re.IGNORECASE) for p in f.patterns)
|
| 174 |
-
area_hit = f.area in lower
|
| 175 |
-
if matched or area_hit:
|
| 176 |
-
hits.append({"id": f.id, "severity": f.severity,
|
| 177 |
-
"title": f.title, "advice": f.advice,
|
| 178 |
-
"pattern_matched": matched})
|
| 179 |
-
return hits
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
def recurring_pattern_hits(text: str) -> list:
|
| 183 |
-
hits = []
|
| 184 |
-
for name, pattern, message in RECURRING_PATTERNS:
|
| 185 |
-
if re.search(pattern, text):
|
| 186 |
-
hits.append({"pattern": name, "message": message})
|
| 187 |
-
return hits
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
def touches_auth(text: str) -> bool:
|
| 191 |
-
return any(re.search(p, text, re.IGNORECASE) for p in AUTH_SIGNALS)
|
| 192 |
-
|
| 193 |
-
|
| 194 |
-
class SecurityReviewChip(BaseSkillChip):
|
| 195 |
-
"""Reviews a request (and any code in it) against the audit's
|
| 196 |
-
confirmed findings and the campus auth architecture. Optionally greps
|
| 197 |
-
the live repo for the same patterns near files the request names."""
|
| 198 |
-
|
| 199 |
-
name = "security_review"
|
| 200 |
-
description = "Audit-findings and auth-impact review"
|
| 201 |
-
version = "2.0.0"
|
| 202 |
-
domain = SkillDomain.SECURITY
|
| 203 |
-
efe_weights = EFEWeights(
|
| 204 |
-
mission_alignment=0.15, stakeholder_benefit=0.30,
|
| 205 |
-
resource_efficiency=0.10, transparency=0.30, equity=0.15,
|
| 206 |
-
)
|
| 207 |
-
capabilities = [SkillCapability.READ_DATA]
|
| 208 |
-
|
| 209 |
-
def __init__(self, repo_files=None):
|
| 210 |
-
super().__init__()
|
| 211 |
-
self.repo_files = repo_files # tools.file_tools.RepoFiles or None
|
| 212 |
-
|
| 213 |
-
async def handle(self, request: SkillRequest,
|
| 214 |
-
context: SkillContext) -> SkillResponse:
|
| 215 |
-
question = context.metadata.get("question", request.raw_input)
|
| 216 |
-
session = context.metadata.get("session")
|
| 217 |
-
|
| 218 |
-
auth = touches_auth(question)
|
| 219 |
-
findings = findings_relevant_to(question)
|
| 220 |
-
recurring = recurring_pattern_hits(question)
|
| 221 |
-
|
| 222 |
-
repo_hits = []
|
| 223 |
-
if self.repo_files is not None and auth:
|
| 224 |
-
# Where does the repo actually enforce auth today?
|
| 225 |
-
for probe in (r"verifyToken", r"requireAdmin"):
|
| 226 |
-
repo_hits.extend(self.repo_files.search(probe)[:5])
|
| 227 |
-
|
| 228 |
-
if session:
|
| 229 |
-
for f in findings:
|
| 230 |
-
session.record_evidence("audit_finding", f["id"], self.name)
|
| 231 |
-
|
| 232 |
-
report = {
|
| 233 |
-
"auth_touched": auth,
|
| 234 |
-
"relevant_findings": findings,
|
| 235 |
-
"recurring_pattern_hits": recurring,
|
| 236 |
-
"repo_auth_sites": repo_hits,
|
| 237 |
-
}
|
| 238 |
-
summary = (
|
| 239 |
-
f"auth_touched={auth}; {len(findings)} audit finding(s) relevant; "
|
| 240 |
-
f"{len(recurring)} recurring pattern hit(s)"
|
| 241 |
-
)
|
| 242 |
-
# NOTE: data IS the artifact — DAGExecutor assigns the whole data
|
| 243 |
-
# dict to this node's single output key ("security_report").
|
| 244 |
-
return SkillResponse(
|
| 245 |
-
content=summary, success=True,
|
| 246 |
-
data=report,
|
| 247 |
-
requires_consensus=auth,
|
| 248 |
-
consensus_action="auth_change_review" if auth else None,
|
| 249 |
-
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|