11const express = require ( "express" ) ;
22const { getDb } = require ( "../db" ) ;
3+ const { sanitizeString } = require ( "../lib/validation" ) ;
34const { parsePagination, wrapRoute } = require ( "../lib/request-helpers" ) ;
45
56const router = express . Router ( ) ;
67
8+ // ── Security limits ─────────────────────────────────────────────────
9+ const MAX_AGENT_NAME_LENGTH = 128 ;
10+ const MAX_WINDOW_HOURS = 720 ; // 30 days max to prevent expensive full-table scans
11+ const MAX_SNAPSHOTS = 10000 ; // cap stored snapshots per agent to prevent disk exhaustion
12+
13+ // Validate agent_name: alphanumeric + common separators, sanitized.
14+ // Rejects empty/null and enforces a length cap to prevent abuse.
15+ function validateAgentName ( name ) {
16+ if ( ! name || typeof name !== "string" ) return null ;
17+ const sanitized = sanitizeString ( name , MAX_AGENT_NAME_LENGTH ) ;
18+ if ( ! sanitized || sanitized . trim ( ) . length === 0 ) return null ;
19+ return sanitized . trim ( ) ;
20+ }
21+
722// ── Schema bootstrap ────────────────────────────────────────────────
823
924let schemaReady = false ;
@@ -52,7 +67,12 @@ const VALID_METRICS = [
5267const VALID_COMPARISONS = [ "lte" , "gte" , "lt" , "gt" , "eq" ] ;
5368
5469function validateTarget ( body ) {
55- if ( ! body . agent_name || typeof body . agent_name !== "string" || body . agent_name . length > 128 ) {
70+ const agentName = validateAgentName ( body . agent_name ) ;
71+ if ( ! agentName ) {
72+ return "agent_name is required (string, max 128 chars, no control characters)" ;
73+ }
74+ body . agent_name = agentName ; // normalize
75+ if ( body . agent_name . length > MAX_AGENT_NAME_LENGTH ) {
5676 return "agent_name is required (string, max 128 chars)" ;
5777 }
5878 if ( ! body . metric || ! VALID_METRICS . includes ( body . metric ) ) {
@@ -132,7 +152,7 @@ function computeAgentMetrics(db, agentName, windowStart, windowEnd) {
132152router . get ( "/targets" , wrapRoute ( "list SLA targets" , ( req , res ) => {
133153 ensureSchema ( ) ;
134154 const db = getDb ( ) ;
135- const { agent_name } = req . query ;
155+ const agent_name = req . query . agent_name ? validateAgentName ( req . query . agent_name ) : null ;
136156
137157 let rows ;
138158 if ( agent_name ) {
@@ -168,10 +188,14 @@ router.put("/targets", wrapRoute("upsert SLA target", (req, res) => {
168188router . delete ( "/targets" , wrapRoute ( "delete SLA target" , ( req , res ) => {
169189 ensureSchema ( ) ;
170190 const db = getDb ( ) ;
171- const { agent_name, metric } = req . query ;
191+ const agent_name = validateAgentName ( req . query . agent_name ) ;
192+ const metric = req . query . metric ;
172193 if ( ! agent_name || ! metric ) {
173194 return res . status ( 400 ) . json ( { error : "agent_name and metric query params required" } ) ;
174195 }
196+ if ( ! VALID_METRICS . includes ( metric ) ) {
197+ return res . status ( 400 ) . json ( { error : "metric must be one of: " + VALID_METRICS . join ( ", " ) } ) ;
198+ }
175199
176200 const result = db . prepare ( "DELETE FROM sla_targets WHERE agent_name = ? AND metric = ?" ) . run ( agent_name , metric ) ;
177201 if ( result . changes === 0 ) return res . status ( 404 ) . json ( { error : "Target not found" } ) ;
@@ -183,12 +207,16 @@ router.delete("/targets", wrapRoute("delete SLA target", (req, res) => {
183207router . post ( "/check" , wrapRoute ( "check SLA compliance" , ( req , res ) => {
184208 ensureSchema ( ) ;
185209 const db = getDb ( ) ;
186- const { agent_name, window_hours } = req . body ;
210+ const agent_name = validateAgentName ( req . body . agent_name ) ;
211+ const { window_hours } = req . body ;
187212
188- if ( ! agent_name || typeof agent_name !== "string" ) {
189- return res . status ( 400 ) . json ( { error : "agent_name required" } ) ;
213+ if ( ! agent_name ) {
214+ return res . status ( 400 ) . json ( { error : "agent_name required (non-empty string) " } ) ;
190215 }
191- const hours = ( typeof window_hours === "number" && window_hours > 0 ) ? window_hours : 24 ;
216+ const hours = Math . min (
217+ ( typeof window_hours === "number" && window_hours > 0 ) ? window_hours : 24 ,
218+ MAX_WINDOW_HOURS
219+ ) ;
192220
193221 const now = new Date ( ) ;
194222 const windowEnd = now . toISOString ( ) ;
@@ -229,6 +257,21 @@ router.post("/check", wrapRoute("check SLA compliance", (req, res) => {
229257 ? Math . round ( ( ( targets . length - violations . length ) / targets . length ) * 10000 ) / 100
230258 : 100 ;
231259
260+ // Cap stored snapshots per agent to prevent unbounded disk growth.
261+ // An attacker repeatedly calling /check could otherwise fill the DB.
262+ const snapshotCount = db . prepare (
263+ "SELECT COUNT(*) as cnt FROM sla_snapshots WHERE agent_name = ?"
264+ ) . get ( agent_name ) . cnt ;
265+ if ( snapshotCount >= MAX_SNAPSHOTS ) {
266+ // Delete oldest snapshots to make room (keep most recent MAX_SNAPSHOTS - 100)
267+ db . prepare ( `
268+ DELETE FROM sla_snapshots WHERE id IN (
269+ SELECT id FROM sla_snapshots WHERE agent_name = ?
270+ ORDER BY created_at ASC LIMIT 100
271+ )
272+ ` ) . run ( agent_name ) ;
273+ }
274+
232275 db . prepare ( `
233276 INSERT INTO sla_snapshots (agent_name, window_start, window_end, metrics, violations, compliance_pct)
234277 VALUES (?, ?, ?, ?, ?, ?)
@@ -250,8 +293,8 @@ router.post("/check", wrapRoute("check SLA compliance", (req, res) => {
250293router . get ( "/history" , wrapRoute ( "list SLA history" , ( req , res ) => {
251294 ensureSchema ( ) ;
252295 const db = getDb ( ) ;
253- const { agent_name } = req . query ;
254- if ( ! agent_name ) return res . status ( 400 ) . json ( { error : "agent_name query param required" } ) ;
296+ const agent_name = validateAgentName ( req . query . agent_name ) ;
297+ if ( ! agent_name ) return res . status ( 400 ) . json ( { error : "agent_name query param required (non-empty string) " } ) ;
255298
256299 const { limit, offset } = parsePagination ( req . query , { defaultLimit : 50 , maxLimit : 200 } ) ;
257300
0 commit comments