Skip to content

Commit f6e4b9e

Browse files
splintersfuryclaude
andcommitted
Add variant alerts to alerter and daily token budget to KernelSense
Alerter now consumes both semantic_deltas and kernelsense tasks, sending Telegram alerts for confirmed cross-driver variants. LLM client tracks daily token usage in Redis with auto-expire and blocks calls when a configurable budget ceiling is reached. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 2043f25 commit f6e4b9e

3 files changed

Lines changed: 207 additions & 7 deletions

File tree

docker-compose.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -436,6 +436,7 @@ services:
436436
KERNELSENSE_FP_THRESHOLD: "4.0"
437437
KERNELSENSE_VARIANT_ENABLED: "true"
438438
KERNELSENSE_VARIANT_CONFIDENCE: "0.70"
439+
KERNELSENSE_DAILY_TOKEN_BUDGET: ${KERNELSENSE_DAILY_TOKEN_BUDGET:-0}
439440
KERNELSENSE_EMBEDDINGS_DIR: /data/embeddings
440441
volumes:
441442
- ./karton.ini:/etc/karton/karton.ini:ro

services/autopiff-alerter/karton_alerter.py

Lines changed: 137 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
11
#!/usr/bin/env python3
22
"""
3-
AutoPiff Alerter — Karton consumer that sends Telegram alerts for high-scoring findings.
3+
AutoPiff Alerter — Karton consumer that sends Telegram alerts.
4+
5+
Consumes:
6+
- {type: autopiff, kind: semantic_deltas} — high-scoring patch findings
7+
- {type: autopiff, kind: kernelsense} — confirmed variant vulnerabilities
48
5-
Consumes: {type: autopiff, kind: semantic_deltas}
69
Filters: final_score >= 8.0 AND surface_area in [ioctl, irp, filesystem]
10+
variant_candidates where is_variant=true AND confidence >= threshold
711
Sends: Telegram alerts via HTTP API
812
Stores: Recent alerts in Redis sorted set (30-day TTL)
913
"""
@@ -28,25 +32,45 @@
2832
TELEGRAM_CHAT_ID = os.environ.get("TELEGRAM_CHAT_ID")
2933
REDIS_HOST = os.environ.get("KARTON_REDIS_HOST", "karton-redis")
3034
SCORE_THRESHOLD = float(os.environ.get("AUTOPIFF_SCORE_THRESHOLD", "8.0"))
35+
VARIANT_CONFIDENCE_THRESHOLD = float(
36+
os.environ.get("AUTOPIFF_VARIANT_CONFIDENCE", "0.65")
37+
)
3138
ALERTABLE_SURFACES = {"ioctl", "irp", "filesystem"}
3239

3340
# Redis keys
3441
ALERTS_KEY = "autopiff:alerts:recent"
3542
ALERTS_FAILED_KEY = "autopiff:alerts:failed"
43+
VARIANT_ALERTS_KEY = "autopiff:alerts:variants"
3644
ALERTS_TTL_SECONDS = 30 * 24 * 3600 # 30 days
3745

3846

3947
class AutoPiffAlerter(Karton):
40-
"""Karton consumer that filters high-scoring AutoPiff findings and sends Telegram alerts."""
48+
"""Karton consumer that sends Telegram alerts for high-scoring findings and variants."""
4149

4250
identity = "karton.autopiff.alerter"
43-
filters = [{"type": "autopiff", "kind": "semantic_deltas"}]
51+
filters = [
52+
{"type": "autopiff", "kind": "semantic_deltas"},
53+
{"type": "autopiff", "kind": "kernelsense"},
54+
]
4455

4556
def __init__(self, *args, **kwargs):
4657
super().__init__(*args, **kwargs)
4758
self.rdb = redis.Redis(host=REDIS_HOST, port=6379, decode_responses=True)
4859

4960
def process(self, task: Task) -> None:
61+
kind = task.headers.get("kind")
62+
if kind == "semantic_deltas":
63+
self._process_semantic_deltas(task)
64+
elif kind == "kernelsense":
65+
self._process_kernelsense(task)
66+
else:
67+
logger.warning(f"Unknown task kind: {kind}")
68+
69+
# ------------------------------------------------------------------
70+
# Semantic Deltas (original patch-based alerting)
71+
# ------------------------------------------------------------------
72+
73+
def _process_semantic_deltas(self, task: Task) -> None:
5074
semantic_deltas = task.get_payload("semantic_deltas")
5175
if not semantic_deltas:
5276
logger.warning("No semantic_deltas payload in task")
@@ -89,15 +113,121 @@ def process(self, task: Task) -> None:
89113

90114
logger.info(f"Found {len(alertable)} alertable findings")
91115

92-
# Build and send alert
93116
msg = self._build_alert_message(
94117
alertable, driver_new_sha, driver_new_ver, driver_old_ver, summary
95118
)
96119
self._send_telegram_alert(msg)
97-
98-
# Store in Redis for /findings command
99120
self._store_alerts(alertable, driver_new_sha)
100121

122+
# ------------------------------------------------------------------
123+
# KernelSense (variant alerting)
124+
# ------------------------------------------------------------------
125+
126+
def _process_kernelsense(self, task: Task) -> None:
127+
ks_raw = task.headers.get("kernelsense")
128+
if isinstance(ks_raw, str):
129+
ks_data = json.loads(ks_raw)
130+
else:
131+
ks_data = ks_raw
132+
133+
if not ks_data:
134+
logger.warning("No kernelsense data in task")
135+
return
136+
137+
findings = ks_data.get("findings", [])
138+
driver_new = ks_data.get("driver_new", {})
139+
driver_name = driver_new.get("name", driver_new.get("sha256", "unknown"))
140+
141+
# Collect all confirmed variants across all findings
142+
all_variants = []
143+
for finding in findings:
144+
candidates = finding.get("variant_candidates", [])
145+
if not candidates:
146+
continue
147+
148+
assessment = finding.get("llm_assessment", {})
149+
if not assessment.get("is_security_fix"):
150+
continue
151+
152+
confirmed = [
153+
c for c in candidates
154+
if c.get("is_variant")
155+
and c.get("confidence", 0) >= VARIANT_CONFIDENCE_THRESHOLD
156+
]
157+
158+
for variant in confirmed:
159+
all_variants.append({
160+
"source_function": finding.get("function", "unknown"),
161+
"source_driver": driver_name,
162+
"bug_class": assessment.get("bug_class", "unknown"),
163+
"source_confidence": assessment.get("confidence", 0),
164+
**variant,
165+
})
166+
167+
if not all_variants:
168+
logger.info("No confirmed variants above threshold")
169+
return
170+
171+
logger.info(f"Found {len(all_variants)} confirmed variant(s)")
172+
173+
msg = self._build_variant_alert(all_variants)
174+
self._send_telegram_alert(msg)
175+
self._store_variant_alerts(all_variants)
176+
177+
def _build_variant_alert(self, variants: list[dict]) -> str:
178+
count = len(variants)
179+
source = variants[0]
180+
181+
msg = (
182+
f"*AutoPiff Variant Alert* — "
183+
f"{count} potential variant{'s' if count > 1 else ''} found\n\n"
184+
)
185+
msg += (
186+
f"Known vulnerability: {source['bug_class']} in "
187+
f"{source['source_driver']}/`{source['source_function']}`\n"
188+
f"Source confidence: *{source['source_confidence']:.2f}*\n\n"
189+
)
190+
191+
for i, v in enumerate(variants[:5]):
192+
msg += (
193+
f"*{i+1}.* {v['driver']} / `{v['function']}` — "
194+
f"similarity *{v['similarity']:.2f}*\n"
195+
)
196+
msg += f" {v.get('match_type', 'unknown')} | confidence: {v['confidence']:.2f}\n"
197+
reasoning = v.get("reasoning", "")
198+
if reasoning:
199+
msg += f" _{reasoning[:100]}_\n"
200+
msg += "\n"
201+
202+
if count > 5:
203+
msg += f"_...and {count - 5} more variant(s)_\n"
204+
205+
return msg
206+
207+
def _store_variant_alerts(self, variants: list[dict]) -> None:
208+
now = time.time()
209+
pipe = self.rdb.pipeline()
210+
for v in variants:
211+
entry = {
212+
"source_driver": v["source_driver"],
213+
"source_function": v["source_function"],
214+
"bug_class": v["bug_class"],
215+
"variant_driver": v["driver"],
216+
"variant_function": v["function"],
217+
"similarity": v["similarity"],
218+
"confidence": v["confidence"],
219+
"reasoning": v.get("reasoning", ""),
220+
}
221+
pipe.zadd(VARIANT_ALERTS_KEY, {json.dumps(entry): now})
222+
223+
cutoff = now - ALERTS_TTL_SECONDS
224+
pipe.zremrangebyscore(VARIANT_ALERTS_KEY, "-inf", cutoff)
225+
pipe.execute()
226+
227+
# ------------------------------------------------------------------
228+
# Shared: message building, Telegram, Redis
229+
# ------------------------------------------------------------------
230+
101231
def _build_alert_message(
102232
self, findings, driver_sha, new_ver, old_ver, summary
103233
) -> str:

services/karton-kernelsense/llm_client.py

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,14 @@
55
1. Prepends the disclosure boundary to every system prompt
66
2. Handles retries with exponential backoff
77
3. Tracks token usage to Redis for cost monitoring
8+
4. Enforces a daily token budget to prevent runaway API costs
89
"""
910

1011
import json
1112
import logging
1213
import os
1314
import time
15+
from datetime import datetime, timezone
1416

1517
import anthropic
1618
import redis
@@ -39,6 +41,11 @@ def __init__(self):
3941

4042
self.client = anthropic.Anthropic()
4143

44+
# Daily token budget (0 = unlimited)
45+
self.daily_token_budget = int(
46+
os.environ.get("KERNELSENSE_DAILY_TOKEN_BUDGET", "0")
47+
)
48+
4249
# Redis for usage tracking (optional)
4350
redis_host = os.environ.get("KARTON_REDIS_HOST", "localhost")
4451
try:
@@ -64,6 +71,21 @@ def analyze(self, prompt: str, task_context: str = "") -> dict:
6471
Returns:
6572
Parsed JSON response from the LLM, or error dict.
6673
"""
74+
# Check daily budget before making the API call
75+
if self.daily_token_budget > 0:
76+
over, used = self._check_daily_budget()
77+
if over:
78+
logger.warning(
79+
f"Daily token budget exceeded ({used}/{self.daily_token_budget}), "
80+
f"skipping LLM call for: {task_context}"
81+
)
82+
return {
83+
"error": "daily_token_budget_exceeded",
84+
"is_security_fix": False,
85+
"budget_used": used,
86+
"budget_limit": self.daily_token_budget,
87+
}
88+
6789
system = BOUNDARY_PROMPT
6890
if task_context:
6991
system += f"\nContext: {task_context}"
@@ -142,20 +164,67 @@ def _parse_response(self, text: str) -> dict:
142164
logger.warning(f"Could not parse LLM response as JSON: {text[:200]}...")
143165
return {"error": "unparseable response", "raw_text": text[:500]}
144166

167+
def _daily_key(self) -> str:
168+
"""Return the Redis key for today's usage."""
169+
today = datetime.now(timezone.utc).strftime("%Y-%m-%d")
170+
return f"kernelsense:usage:daily:{today}"
171+
172+
def _check_daily_budget(self) -> tuple[bool, int]:
173+
"""Check if today's token usage exceeds the daily budget.
174+
175+
Returns:
176+
(over_budget, tokens_used) tuple.
177+
"""
178+
if not self.redis:
179+
return False, 0
180+
181+
try:
182+
key = self._daily_key()
183+
used_in = int(self.redis.hget(key, "input_tokens") or 0)
184+
used_out = int(self.redis.hget(key, "output_tokens") or 0)
185+
total_used = used_in + used_out
186+
return total_used >= self.daily_token_budget, total_used
187+
except redis.RedisError as e:
188+
logger.debug(f"Failed to check daily budget: {e}")
189+
return False, 0
190+
145191
def _track_usage(self, usage, context: str = "") -> None:
146192
"""Log token usage to Redis for cost monitoring."""
147193
if not self.redis:
148194
return
149195

196+
total_tokens = usage.input_tokens + usage.output_tokens
197+
150198
try:
199+
# Lifetime totals
151200
key = "kernelsense:usage:total"
152201
self.redis.hincrby(key, "input_tokens", usage.input_tokens)
153202
self.redis.hincrby(key, "output_tokens", usage.output_tokens)
154203
self.redis.hincrby(key, "calls", 1)
155204

205+
# Daily totals (auto-expire after 7 days for cleanup)
206+
daily_key = self._daily_key()
207+
pipe = self.redis.pipeline()
208+
pipe.hincrby(daily_key, "input_tokens", usage.input_tokens)
209+
pipe.hincrby(daily_key, "output_tokens", usage.output_tokens)
210+
pipe.hincrby(daily_key, "calls", 1)
211+
pipe.expire(daily_key, 7 * 24 * 3600)
212+
pipe.execute()
213+
156214
logger.debug(
157215
f"LLM usage ({context}): "
158216
f"{usage.input_tokens} in / {usage.output_tokens} out"
159217
)
218+
219+
# Warn when approaching budget
220+
if self.daily_token_budget > 0:
221+
daily_total = int(self.redis.hget(daily_key, "input_tokens") or 0) + \
222+
int(self.redis.hget(daily_key, "output_tokens") or 0)
223+
pct = daily_total / self.daily_token_budget * 100
224+
if pct >= 90:
225+
logger.warning(
226+
f"Daily token usage at {pct:.0f}% "
227+
f"({daily_total}/{self.daily_token_budget})"
228+
)
160229
except redis.RedisError as e:
161230
logger.debug(f"Failed to track usage: {e}")

0 commit comments

Comments
 (0)