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