Skip to content

Commit 14f26b5

Browse files
feat(compliance): Add adaptive compliance scheduler and system info collection (#273)
Merging with admin override - CI failures are pre-existing test issues unrelated to this PR
1 parent d7b64e3 commit 14f26b5

18 files changed

Lines changed: 3250 additions & 68 deletions

.secrets.baseline

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -265,7 +265,7 @@
265265
"filename": "frontend/src/pages/settings/Settings.tsx",
266266
"hashed_secret": "27c6929aef41ae2bcadac15ca6abcaff72cda9cd",
267267
"is_verified": false,
268-
"line_number": 1192
268+
"line_number": 1196
269269
}
270270
],
271271
"packaging/bundle/create-prebuilt-images.sh": [
@@ -430,5 +430,5 @@
430430
}
431431
]
432432
},
433-
"generated_at": "2026-02-04T17:48:00Z"
433+
"generated_at": "2026-02-10T03:59:54Z"
434434
}
Lines changed: 321 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,321 @@
1+
"""Add compliance scheduler tables for adaptive compliance scanning
2+
3+
Revision ID: 026_compliance_scheduler
4+
Revises: 025_host_monitoring_history
5+
Create Date: 2026-02-09
6+
7+
Creates tables for the Adaptive Compliance Scheduler:
8+
- host_compliance_schedule: Per-host scheduling state and next scan time
9+
- compliance_scheduler_config: Global scheduler configuration (intervals, priorities)
10+
11+
This enables automatic compliance scanning with state-based intervals (max 48 hours).
12+
"""
13+
14+
import sqlalchemy as sa
15+
from sqlalchemy.dialects import postgresql
16+
17+
from alembic import op
18+
19+
# Revision identifiers
20+
revision = "026_compliance_scheduler"
21+
down_revision = "025_host_monitoring_history"
22+
branch_labels = None
23+
depends_on = None
24+
25+
26+
def upgrade() -> None:
27+
"""Create compliance scheduler tables."""
28+
conn = op.get_bind()
29+
30+
# 1. Create host_compliance_schedule table
31+
result = conn.execute(
32+
sa.text(
33+
"SELECT EXISTS (SELECT FROM information_schema.tables " "WHERE table_name = 'host_compliance_schedule')"
34+
)
35+
)
36+
if not result.scalar():
37+
op.create_table(
38+
"host_compliance_schedule",
39+
sa.Column(
40+
"id",
41+
postgresql.UUID(as_uuid=True),
42+
primary_key=True,
43+
server_default=sa.text("gen_random_uuid()"),
44+
),
45+
sa.Column(
46+
"host_id",
47+
postgresql.UUID(as_uuid=True),
48+
sa.ForeignKey("hosts.id", ondelete="CASCADE"),
49+
nullable=False,
50+
unique=True,
51+
),
52+
# Current compliance state
53+
sa.Column(
54+
"compliance_score",
55+
sa.Float,
56+
nullable=True,
57+
),
58+
sa.Column(
59+
"compliance_state",
60+
sa.String(20),
61+
nullable=False,
62+
server_default="unknown",
63+
),
64+
sa.Column(
65+
"has_critical_findings",
66+
sa.Boolean,
67+
nullable=False,
68+
server_default="false",
69+
),
70+
sa.Column(
71+
"pass_count",
72+
sa.Integer,
73+
nullable=True,
74+
),
75+
sa.Column(
76+
"fail_count",
77+
sa.Integer,
78+
nullable=True,
79+
),
80+
# Scheduling
81+
sa.Column(
82+
"current_interval_minutes",
83+
sa.Integer,
84+
nullable=False,
85+
server_default="1440", # 24 hours default
86+
),
87+
sa.Column(
88+
"next_scheduled_scan",
89+
sa.DateTime(timezone=True),
90+
nullable=True,
91+
),
92+
sa.Column(
93+
"last_scan_completed",
94+
sa.DateTime(timezone=True),
95+
nullable=True,
96+
),
97+
sa.Column(
98+
"last_scan_id",
99+
postgresql.UUID(as_uuid=True),
100+
nullable=True,
101+
),
102+
# Maintenance mode
103+
sa.Column(
104+
"maintenance_mode",
105+
sa.Boolean,
106+
nullable=False,
107+
server_default="false",
108+
),
109+
sa.Column(
110+
"maintenance_until",
111+
sa.DateTime(timezone=True),
112+
nullable=True,
113+
),
114+
# Scan priority (1-10, higher = more urgent)
115+
sa.Column(
116+
"scan_priority",
117+
sa.Integer,
118+
nullable=False,
119+
server_default="5",
120+
),
121+
# Failure tracking
122+
sa.Column(
123+
"consecutive_scan_failures",
124+
sa.Integer,
125+
nullable=False,
126+
server_default="0",
127+
),
128+
# Audit timestamps
129+
sa.Column(
130+
"created_at",
131+
sa.DateTime(timezone=True),
132+
nullable=False,
133+
server_default=sa.text("CURRENT_TIMESTAMP"),
134+
),
135+
sa.Column(
136+
"updated_at",
137+
sa.DateTime(timezone=True),
138+
nullable=False,
139+
server_default=sa.text("CURRENT_TIMESTAMP"),
140+
),
141+
)
142+
143+
# Index for finding hosts due for scanning
144+
op.create_index(
145+
"ix_compliance_schedule_next_scan",
146+
"host_compliance_schedule",
147+
["next_scheduled_scan"],
148+
postgresql_where=sa.text("maintenance_mode = false"),
149+
)
150+
151+
# Index for querying by compliance state
152+
op.create_index(
153+
"ix_compliance_schedule_state",
154+
"host_compliance_schedule",
155+
["compliance_state"],
156+
)
157+
158+
# Index for priority-based scanning
159+
op.create_index(
160+
"ix_compliance_schedule_priority",
161+
"host_compliance_schedule",
162+
["scan_priority"],
163+
)
164+
165+
# 2. Create compliance_scheduler_config table (singleton config)
166+
result = conn.execute(
167+
sa.text(
168+
"SELECT EXISTS (SELECT FROM information_schema.tables " "WHERE table_name = 'compliance_scheduler_config')"
169+
)
170+
)
171+
if not result.scalar():
172+
op.create_table(
173+
"compliance_scheduler_config",
174+
sa.Column(
175+
"id",
176+
sa.Integer,
177+
primary_key=True,
178+
),
179+
sa.Column(
180+
"enabled",
181+
sa.Boolean,
182+
nullable=False,
183+
server_default="true",
184+
),
185+
# Intervals in minutes for each compliance state
186+
sa.Column(
187+
"interval_compliant",
188+
sa.Integer,
189+
nullable=False,
190+
server_default="1440", # 24 hours
191+
),
192+
sa.Column(
193+
"interval_mostly_compliant",
194+
sa.Integer,
195+
nullable=False,
196+
server_default="720", # 12 hours
197+
),
198+
sa.Column(
199+
"interval_partial",
200+
sa.Integer,
201+
nullable=False,
202+
server_default="360", # 6 hours
203+
),
204+
sa.Column(
205+
"interval_low",
206+
sa.Integer,
207+
nullable=False,
208+
server_default="120", # 2 hours
209+
),
210+
sa.Column(
211+
"interval_critical",
212+
sa.Integer,
213+
nullable=False,
214+
server_default="60", # 1 hour
215+
),
216+
sa.Column(
217+
"interval_unknown",
218+
sa.Integer,
219+
nullable=False,
220+
server_default="0", # Immediate
221+
),
222+
sa.Column(
223+
"interval_maintenance",
224+
sa.Integer,
225+
nullable=False,
226+
server_default="2880", # 48 hours (max)
227+
),
228+
# Maximum interval (enforced ceiling)
229+
sa.Column(
230+
"max_interval_minutes",
231+
sa.Integer,
232+
nullable=False,
233+
server_default="2880", # 48 hours
234+
),
235+
# Priority levels (1-10, higher = more urgent)
236+
sa.Column(
237+
"priority_compliant",
238+
sa.Integer,
239+
nullable=False,
240+
server_default="3",
241+
),
242+
sa.Column(
243+
"priority_mostly_compliant",
244+
sa.Integer,
245+
nullable=False,
246+
server_default="4",
247+
),
248+
sa.Column(
249+
"priority_partial",
250+
sa.Integer,
251+
nullable=False,
252+
server_default="6",
253+
),
254+
sa.Column(
255+
"priority_low",
256+
sa.Integer,
257+
nullable=False,
258+
server_default="7",
259+
),
260+
sa.Column(
261+
"priority_critical",
262+
sa.Integer,
263+
nullable=False,
264+
server_default="9",
265+
),
266+
sa.Column(
267+
"priority_unknown",
268+
sa.Integer,
269+
nullable=False,
270+
server_default="10",
271+
),
272+
sa.Column(
273+
"priority_maintenance",
274+
sa.Integer,
275+
nullable=False,
276+
server_default="1",
277+
),
278+
# Concurrency and timeout settings
279+
sa.Column(
280+
"max_concurrent_scans",
281+
sa.Integer,
282+
nullable=False,
283+
server_default="5",
284+
),
285+
sa.Column(
286+
"scan_timeout_seconds",
287+
sa.Integer,
288+
nullable=False,
289+
server_default="600", # 10 minutes
290+
),
291+
# Audit
292+
sa.Column(
293+
"updated_at",
294+
sa.DateTime(timezone=True),
295+
nullable=False,
296+
server_default=sa.text("CURRENT_TIMESTAMP"),
297+
),
298+
)
299+
300+
# Insert default configuration (id=1)
301+
conn.execute(
302+
sa.text(
303+
"""
304+
INSERT INTO compliance_scheduler_config (id)
305+
VALUES (1)
306+
ON CONFLICT (id) DO NOTHING
307+
"""
308+
)
309+
)
310+
311+
312+
def downgrade() -> None:
313+
"""Drop compliance scheduler tables."""
314+
# Drop indexes first
315+
op.drop_index("ix_compliance_schedule_priority", table_name="host_compliance_schedule")
316+
op.drop_index("ix_compliance_schedule_state", table_name="host_compliance_schedule")
317+
op.drop_index("ix_compliance_schedule_next_scan", table_name="host_compliance_schedule")
318+
319+
# Drop tables
320+
op.drop_table("compliance_scheduler_config")
321+
op.drop_table("host_compliance_schedule")

0 commit comments

Comments
 (0)