@grc-claw/drift-detector 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,37 @@
1
+ import type { BaselineSnapshot, ControlEvaluator, DriftAlert, DriftDetectionResult, DriftDetectorConfig, DriftEvent, DriftEventType, DriftSeverity } from './types.js';
2
+ export declare class DriftDetector {
3
+ private config;
4
+ private evaluator;
5
+ private baselineHistory;
6
+ private currentBaseline;
7
+ private driftHistory;
8
+ private alertHistory;
9
+ private pollTimer;
10
+ constructor(config: DriftDetectorConfig, evaluator: ControlEvaluator);
11
+ /** Capture a baseline snapshot of all controls across configured frameworks */
12
+ captureBaseline(): Promise<BaselineSnapshot>;
13
+ /** Run a single drift detection cycle against the current baseline */
14
+ detectDrift(): Promise<DriftDetectionResult>;
15
+ /** Start continuous polling at the configured interval */
16
+ startPolling(): void;
17
+ /** Stop continuous polling */
18
+ stopPolling(): void;
19
+ /** Get the drift history */
20
+ getDriftHistory(): DriftEvent[];
21
+ /** Get the alert history */
22
+ getAlertHistory(): DriftAlert[];
23
+ /** Get the current baseline */
24
+ getCurrentBaseline(): BaselineSnapshot | null;
25
+ /** Get baseline history */
26
+ getBaselineHistory(): BaselineSnapshot[];
27
+ /** Compute a summary of drift events by severity */
28
+ getDriftSummary(): {
29
+ totalEvents: number;
30
+ bySeverity: Record<DriftSeverity, number>;
31
+ byType: Record<DriftEventType, number>;
32
+ byFramework: Record<string, number>;
33
+ overallScoreDelta: number;
34
+ };
35
+ private buildAlerts;
36
+ private computeHash;
37
+ }
@@ -0,0 +1,284 @@
1
+ import { createHash, randomUUID } from 'node:crypto';
2
+ // ─── Status Severity Ranking ────────────────────────────────────────
3
+ const STATUS_SEVERITY = {
4
+ compliant: 4,
5
+ partial: 3,
6
+ non_compliant: 1,
7
+ unknown: 0,
8
+ };
9
+ function severityFromDelta(delta) {
10
+ const absDelta = Math.abs(delta);
11
+ if (absDelta >= 3)
12
+ return 'critical';
13
+ if (absDelta >= 2)
14
+ return 'high';
15
+ if (absDelta >= 1)
16
+ return 'medium';
17
+ return 'low';
18
+ }
19
+ function priorityFromSeverity(severity) {
20
+ switch (severity) {
21
+ case 'critical': return 'p1';
22
+ case 'high': return 'p2';
23
+ case 'medium': return 'p3';
24
+ case 'low': return 'p4';
25
+ }
26
+ }
27
+ function classifyDriftEvent(prev, curr) {
28
+ const prevStatusRank = STATUS_SEVERITY[prev.status];
29
+ const currStatusRank = STATUS_SEVERITY[curr.status];
30
+ if (currStatusRank < prevStatusRank) {
31
+ const evType = prev.evidenceCount > curr.evidenceCount
32
+ ? 'evidence_revoked'
33
+ : curr.status === 'unknown'
34
+ ? 'control_downgraded'
35
+ : 'status_change';
36
+ return {
37
+ eventType: evType,
38
+ description: `${prev.controlId}: status ${prev.status} → ${curr.status} (evidence ${prev.evidenceCount} → ${curr.evidenceCount})`,
39
+ };
40
+ }
41
+ if (curr.evidenceCount < prev.evidenceCount) {
42
+ return {
43
+ eventType: 'evidence_expired',
44
+ description: `${prev.controlId}: evidence count decreased ${prev.evidenceCount} → ${curr.evidenceCount}`,
45
+ };
46
+ }
47
+ if (curr.complianceScore < prev.complianceScore) {
48
+ return {
49
+ eventType: 'score_degradation',
50
+ description: `${prev.controlId}: score decreased ${prev.complianceScore} → ${curr.complianceScore}`,
51
+ };
52
+ }
53
+ if (prev.status === 'unknown' && curr.status !== 'unknown') {
54
+ return {
55
+ eventType: 'new_gap',
56
+ description: `${prev.controlId}: status changed from unknown to ${curr.status}`,
57
+ };
58
+ }
59
+ return null;
60
+ }
61
+ // ─── Drift Detector ─────────────────────────────────────────────────
62
+ export class DriftDetector {
63
+ config;
64
+ evaluator;
65
+ baselineHistory = [];
66
+ currentBaseline = null;
67
+ driftHistory = [];
68
+ alertHistory = [];
69
+ pollTimer = null;
70
+ constructor(config, evaluator) {
71
+ this.config = {
72
+ driftThresholdPercent: config.driftThresholdPercent ?? 5,
73
+ scoreDeltaAlertThreshold: config.scoreDeltaAlertThreshold ?? 10,
74
+ pollIntervalMs: config.pollIntervalMs ?? 60_000,
75
+ maxBaselineHistory: config.maxBaselineHistory ?? 50,
76
+ onDrift: config.onDrift ?? (() => { }),
77
+ onAlert: config.onAlert ?? (() => { }),
78
+ tenantId: config.tenantId,
79
+ frameworks: config.frameworks,
80
+ };
81
+ this.evaluator = evaluator;
82
+ }
83
+ /** Capture a baseline snapshot of all controls across configured frameworks */
84
+ async captureBaseline() {
85
+ const allControls = [];
86
+ let totalScore = 0;
87
+ let compliantCount = 0;
88
+ for (const framework of this.config.frameworks) {
89
+ const definitions = await this.evaluator.listControls(framework);
90
+ for (const def of definitions) {
91
+ const snapshot = await this.evaluator.evaluateControl(def.controlId, framework);
92
+ allControls.push(snapshot);
93
+ totalScore += snapshot.complianceScore;
94
+ if (snapshot.status === 'compliant')
95
+ compliantCount++;
96
+ }
97
+ }
98
+ const overallScore = allControls.length > 0
99
+ ? Math.round((totalScore / allControls.length) * 100) / 100
100
+ : 0;
101
+ const frameworkScores = {};
102
+ for (const fw of this.config.frameworks) {
103
+ const fwControls = allControls.filter(c => c.framework === fw);
104
+ frameworkScores[fw] = fwControls.length > 0
105
+ ? Math.round((fwControls.reduce((s, c) => s + c.complianceScore, 0) / fwControls.length) * 100) / 100
106
+ : 0;
107
+ }
108
+ const baseline = {
109
+ id: randomUUID(),
110
+ capturedAt: new Date().toISOString(),
111
+ controls: allControls,
112
+ overallScore,
113
+ frameworkScores,
114
+ controlCount: allControls.length,
115
+ compliantCount,
116
+ hash: this.computeHash(allControls),
117
+ };
118
+ this.currentBaseline = baseline;
119
+ this.baselineHistory.push(baseline);
120
+ if (this.baselineHistory.length > this.config.maxBaselineHistory) {
121
+ this.baselineHistory.shift();
122
+ }
123
+ return baseline;
124
+ }
125
+ /** Run a single drift detection cycle against the current baseline */
126
+ async detectDrift() {
127
+ if (!this.currentBaseline) {
128
+ throw new Error('No baseline captured. Call captureBaseline() first.');
129
+ }
130
+ const baseline = this.currentBaseline;
131
+ const currentControls = [];
132
+ let totalScore = 0;
133
+ for (const framework of this.config.frameworks) {
134
+ const definitions = await this.evaluator.listControls(framework);
135
+ for (const def of definitions) {
136
+ const snapshot = await this.evaluator.evaluateControl(def.controlId, framework);
137
+ currentControls.push(snapshot);
138
+ totalScore += snapshot.complianceScore;
139
+ }
140
+ }
141
+ const currentScore = currentControls.length > 0
142
+ ? Math.round((totalScore / currentControls.length) * 100) / 100
143
+ : 0;
144
+ const baselineMap = new Map(baseline.controls.map(c => [`${c.controlId}:${c.framework}`, c]));
145
+ const driftEvents = [];
146
+ for (const curr of currentControls) {
147
+ const key = `${curr.controlId}:${curr.framework}`;
148
+ const prev = baselineMap.get(key);
149
+ if (!prev)
150
+ continue;
151
+ const classification = classifyDriftEvent(prev, curr);
152
+ if (!classification)
153
+ continue;
154
+ const statusDelta = STATUS_SEVERITY[prev.status] - STATUS_SEVERITY[curr.status];
155
+ const scoreDelta = prev.complianceScore - curr.complianceScore;
156
+ const severity = severityFromDelta(Math.max(statusDelta, scoreDelta));
157
+ driftEvents.push({
158
+ id: randomUUID(),
159
+ timestamp: new Date().toISOString(),
160
+ controlId: curr.controlId,
161
+ framework: curr.framework,
162
+ eventType: classification.eventType,
163
+ previousStatus: prev.status,
164
+ currentStatus: curr.status,
165
+ previousEvidenceCount: prev.evidenceCount,
166
+ currentEvidenceCount: curr.evidenceCount,
167
+ previousScore: prev.complianceScore,
168
+ currentScore: curr.complianceScore,
169
+ delta: scoreDelta,
170
+ severity,
171
+ description: classification.description,
172
+ });
173
+ }
174
+ const overallScoreDelta = baseline.overallScore - currentScore;
175
+ const driftDetected = driftEvents.length > 0 ||
176
+ Math.abs(overallScoreDelta) >= this.config.driftThresholdPercent;
177
+ if (driftDetected) {
178
+ this.driftHistory.push(...driftEvents);
179
+ this.config.onDrift(driftEvents);
180
+ }
181
+ const alerts = this.buildAlerts(driftEvents, overallScoreDelta);
182
+ for (const alert of alerts) {
183
+ this.alertHistory.push(alert);
184
+ this.config.onAlert(alert);
185
+ }
186
+ return {
187
+ snapshotId: randomUUID(),
188
+ baselineId: baseline.id,
189
+ detectedAt: new Date().toISOString(),
190
+ driftEvents,
191
+ driftDetected,
192
+ overallScoreDelta: Math.round(overallScoreDelta * 100) / 100,
193
+ currentScore,
194
+ baselineScore: baseline.overallScore,
195
+ alerts,
196
+ };
197
+ }
198
+ /** Start continuous polling at the configured interval */
199
+ startPolling() {
200
+ if (this.pollTimer)
201
+ return;
202
+ this.pollTimer = setInterval(() => {
203
+ this.detectDrift().catch(err => {
204
+ console.error('[DriftDetector] polling error:', err instanceof Error ? err.message : err);
205
+ });
206
+ }, this.config.pollIntervalMs);
207
+ }
208
+ /** Stop continuous polling */
209
+ stopPolling() {
210
+ if (this.pollTimer) {
211
+ clearInterval(this.pollTimer);
212
+ this.pollTimer = null;
213
+ }
214
+ }
215
+ /** Get the drift history */
216
+ getDriftHistory() {
217
+ return [...this.driftHistory];
218
+ }
219
+ /** Get the alert history */
220
+ getAlertHistory() {
221
+ return [...this.alertHistory];
222
+ }
223
+ /** Get the current baseline */
224
+ getCurrentBaseline() {
225
+ return this.currentBaseline;
226
+ }
227
+ /** Get baseline history */
228
+ getBaselineHistory() {
229
+ return [...this.baselineHistory];
230
+ }
231
+ /** Compute a summary of drift events by severity */
232
+ getDriftSummary() {
233
+ const bySeverity = { critical: 0, high: 0, medium: 0, low: 0 };
234
+ const byType = {
235
+ evidence_revoked: 0,
236
+ control_downgraded: 0,
237
+ evidence_expired: 0,
238
+ new_gap: 0,
239
+ score_degradation: 0,
240
+ status_change: 0,
241
+ };
242
+ const byFramework = {};
243
+ for (const event of this.driftHistory) {
244
+ bySeverity[event.severity]++;
245
+ byType[event.eventType]++;
246
+ byFramework[event.framework] = (byFramework[event.framework] ?? 0) + 1;
247
+ }
248
+ const overallScoreDelta = this.baselineHistory.length >= 2
249
+ ? this.baselineHistory[0].overallScore - this.baselineHistory[this.baselineHistory.length - 1].overallScore
250
+ : 0;
251
+ return {
252
+ totalEvents: this.driftHistory.length,
253
+ bySeverity,
254
+ byType,
255
+ byFramework,
256
+ overallScoreDelta: Math.round(overallScoreDelta * 100) / 100,
257
+ };
258
+ }
259
+ buildAlerts(events, overallDelta) {
260
+ if (events.length === 0 && Math.abs(overallDelta) < this.config.scoreDeltaAlertThreshold) {
261
+ return [];
262
+ }
263
+ const alerts = [];
264
+ const severityOrder = ['critical', 'high', 'medium', 'low'];
265
+ const maxSeverity = severityOrder.find(s => events.some(e => e.severity === s)) ?? 'low';
266
+ const affectedFrameworks = [...new Set(events.map(e => e.framework))];
267
+ alerts.push({
268
+ id: randomUUID(),
269
+ timestamp: new Date().toISOString(),
270
+ priority: priorityFromSeverity(maxSeverity),
271
+ driftEvents: events,
272
+ summary: `${events.length} drift event(s) detected across ${affectedFrameworks.length} framework(s). Score delta: ${overallDelta > 0 ? '-' : '+'}${Math.abs(overallDelta).toFixed(1)}%`,
273
+ affectedFrameworks,
274
+ affectedControlCount: new Set(events.map(e => e.controlId)).size,
275
+ maxSeverity,
276
+ overallScoreDelta: Math.round(overallDelta * 100) / 100,
277
+ });
278
+ return alerts;
279
+ }
280
+ computeHash(controls) {
281
+ const payload = JSON.stringify(controls.map(c => `${c.controlId}:${c.status}:${c.evidenceCount}:${c.complianceScore}`).sort());
282
+ return createHash('sha256').update(payload).digest('hex');
283
+ }
284
+ }
@@ -0,0 +1,2 @@
1
+ export { DriftDetector } from './DriftDetector.js';
2
+ export type { AlertPriority, BaselineSnapshot, ControlEvaluator, ControlSnapshot, ControlStatus, DriftAlert, DriftDetectionResult, DriftDetectorConfig, DriftEvent, DriftEventType, DriftSeverity, } from './types.js';
package/dist/index.js ADDED
@@ -0,0 +1 @@
1
+ export { DriftDetector } from './DriftDetector.js';
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,155 @@
1
+ import { describe, it, beforeEach } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { DriftDetector } from './DriftDetector.js';
4
+ // ─── Mock Evaluator ─────────────────────────────────────────────────
5
+ function makeMockEvaluator(controls) {
6
+ return {
7
+ async evaluateControl(controlId, framework) {
8
+ const key = `${controlId}:${framework}`;
9
+ const snap = controls.get(key);
10
+ if (!snap) {
11
+ return {
12
+ controlId,
13
+ framework,
14
+ status: 'unknown',
15
+ evidenceHashes: [],
16
+ evidenceCount: 0,
17
+ complianceScore: 0,
18
+ lastCheckedAt: new Date().toISOString(),
19
+ };
20
+ }
21
+ return { ...snap, lastCheckedAt: new Date().toISOString() };
22
+ },
23
+ async listControls(framework) {
24
+ const defs = [];
25
+ for (const [key, snap] of controls) {
26
+ if (snap.framework === framework) {
27
+ defs.push({ controlId: snap.controlId, title: snap.controlId });
28
+ }
29
+ }
30
+ return defs;
31
+ },
32
+ };
33
+ }
34
+ function makeControl(controlId, framework, status, evidenceCount, score) {
35
+ return {
36
+ controlId,
37
+ framework,
38
+ status,
39
+ evidenceHashes: Array.from({ length: evidenceCount }, (_, i) => `hash-${i}`),
40
+ evidenceCount,
41
+ complianceScore: score,
42
+ lastCheckedAt: new Date().toISOString(),
43
+ };
44
+ }
45
+ // ─── Tests ──────────────────────────────────────────────────────────
46
+ describe('DriftDetector', () => {
47
+ let controls;
48
+ let evaluator;
49
+ let config;
50
+ beforeEach(() => {
51
+ controls = new Map();
52
+ controls.set('A.5.1:iso27001', makeControl('A.5.1', 'iso27001', 'compliant', 3, 100));
53
+ controls.set('A.5.2:iso27001', makeControl('A.5.2', 'iso27001', 'compliant', 2, 100));
54
+ controls.set('A.8.1:iso27001', makeControl('A.8.1', 'iso27001', 'partial', 1, 50));
55
+ evaluator = makeMockEvaluator(controls);
56
+ config = {
57
+ tenantId: 1,
58
+ frameworks: ['iso27001'],
59
+ driftThresholdPercent: 5,
60
+ scoreDeltaAlertThreshold: 10,
61
+ };
62
+ });
63
+ it('should capture a baseline snapshot', async () => {
64
+ const detector = new DriftDetector(config, evaluator);
65
+ const baseline = await detector.captureBaseline();
66
+ assert.equal(baseline.controlCount, 3);
67
+ assert.equal(baseline.compliantCount, 2);
68
+ assert.ok(baseline.hash.length > 0);
69
+ assert.ok(baseline.id.length > 0);
70
+ });
71
+ it('should detect no drift when controls are unchanged', async () => {
72
+ const detector = new DriftDetector(config, evaluator);
73
+ await detector.captureBaseline();
74
+ const result = await detector.detectDrift();
75
+ assert.equal(result.driftDetected, false);
76
+ assert.equal(result.driftEvents.length, 0);
77
+ assert.equal(result.overallScoreDelta, 0);
78
+ });
79
+ it('should detect status downgrade drift', async () => {
80
+ const detector = new DriftDetector(config, evaluator);
81
+ await detector.captureBaseline();
82
+ controls.set('A.5.1:iso27001', makeControl('A.5.1', 'iso27001', 'non_compliant', 0, 0));
83
+ const result = await detector.detectDrift();
84
+ assert.equal(result.driftDetected, true);
85
+ assert.ok(result.driftEvents.length > 0);
86
+ const event = result.driftEvents.find(e => e.controlId === 'A.5.1');
87
+ assert.ok(event);
88
+ assert.equal(event.previousStatus, 'compliant');
89
+ assert.equal(event.currentStatus, 'non_compliant');
90
+ assert.equal(event.eventType, 'evidence_revoked');
91
+ });
92
+ it('should detect evidence revocation', async () => {
93
+ const detector = new DriftDetector(config, evaluator);
94
+ await detector.captureBaseline();
95
+ controls.set('A.5.1:iso27001', makeControl('A.5.1', 'iso27001', 'compliant', 0, 100));
96
+ const result = await detector.detectDrift();
97
+ const event = result.driftEvents.find(e => e.controlId === 'A.5.1');
98
+ assert.ok(event);
99
+ assert.equal(event.eventType, 'evidence_expired');
100
+ assert.equal(event.previousEvidenceCount, 3);
101
+ assert.equal(event.currentEvidenceCount, 0);
102
+ });
103
+ it('should detect score degradation', async () => {
104
+ const detector = new DriftDetector(config, evaluator);
105
+ await detector.captureBaseline();
106
+ controls.set('A.8.1:iso27001', makeControl('A.8.1', 'iso27001', 'partial', 1, 25));
107
+ const result = await detector.detectDrift();
108
+ const event = result.driftEvents.find(e => e.controlId === 'A.8.1');
109
+ assert.ok(event);
110
+ assert.equal(event.eventType, 'score_degradation');
111
+ assert.equal(event.delta, 25);
112
+ });
113
+ it('should generate alerts when drift exceeds threshold', async () => {
114
+ let alertReceived = false;
115
+ config.onAlert = () => { alertReceived = true; };
116
+ const detector = new DriftDetector(config, evaluator);
117
+ await detector.captureBaseline();
118
+ controls.set('A.5.1:iso27001', makeControl('A.5.1', 'iso27001', 'non_compliant', 0, 0));
119
+ await detector.detectDrift();
120
+ assert.equal(alertReceived, true);
121
+ const alerts = detector.getAlertHistory();
122
+ assert.ok(alerts.length > 0);
123
+ assert.equal(alerts[0].priority, 'p1');
124
+ });
125
+ it('should track drift history across multiple cycles', async () => {
126
+ const detector = new DriftDetector(config, evaluator);
127
+ await detector.captureBaseline();
128
+ controls.set('A.5.1:iso27001', makeControl('A.5.1', 'iso27001', 'non_compliant', 0, 0));
129
+ await detector.detectDrift();
130
+ controls.set('A.5.2:iso27001', makeControl('A.5.2', 'iso27001', 'non_compliant', 0, 0));
131
+ await detector.detectDrift();
132
+ const history = detector.getDriftHistory();
133
+ assert.ok(history.length >= 2);
134
+ });
135
+ it('should compute drift summary correctly', async () => {
136
+ const detector = new DriftDetector(config, evaluator);
137
+ await detector.captureBaseline();
138
+ controls.set('A.5.1:iso27001', makeControl('A.5.1', 'iso27001', 'non_compliant', 0, 0));
139
+ controls.set('A.5.2:iso27001', makeControl('A.5.2', 'iso27001', 'partial', 1, 50));
140
+ await detector.detectDrift();
141
+ const summary = detector.getDriftSummary();
142
+ assert.ok(summary.totalEvents >= 2);
143
+ assert.ok(summary.bySeverity.critical > 0 || summary.bySeverity.high > 0);
144
+ assert.ok(summary.byFramework.iso27001 >= 2);
145
+ });
146
+ it('should maintain baseline history within limits', async () => {
147
+ const limitedConfig = { ...config, maxBaselineHistory: 3 };
148
+ const detector = new DriftDetector(limitedConfig, evaluator);
149
+ for (let i = 0; i < 5; i++) {
150
+ await detector.captureBaseline();
151
+ }
152
+ const history = detector.getBaselineHistory();
153
+ assert.equal(history.length, 3);
154
+ });
155
+ });
@@ -0,0 +1,80 @@
1
+ export type ControlStatus = 'compliant' | 'non_compliant' | 'partial' | 'unknown';
2
+ export type DriftSeverity = 'critical' | 'high' | 'medium' | 'low';
3
+ export type DriftEventType = 'evidence_revoked' | 'control_downgraded' | 'evidence_expired' | 'new_gap' | 'score_degradation' | 'status_change';
4
+ export type AlertPriority = 'p1' | 'p2' | 'p3' | 'p4';
5
+ export interface ControlSnapshot {
6
+ controlId: string;
7
+ framework: string;
8
+ status: ControlStatus;
9
+ evidenceHashes: string[];
10
+ evidenceCount: number;
11
+ complianceScore: number;
12
+ lastCheckedAt: string;
13
+ metadata?: Record<string, unknown>;
14
+ }
15
+ export interface DriftEvent {
16
+ id: string;
17
+ timestamp: string;
18
+ controlId: string;
19
+ framework: string;
20
+ eventType: DriftEventType;
21
+ previousStatus: ControlStatus;
22
+ currentStatus: ControlStatus;
23
+ previousEvidenceCount: number;
24
+ currentEvidenceCount: number;
25
+ previousScore: number;
26
+ currentScore: number;
27
+ delta: number;
28
+ severity: DriftSeverity;
29
+ description: string;
30
+ metadata?: Record<string, unknown>;
31
+ }
32
+ export interface DriftAlert {
33
+ id: string;
34
+ timestamp: string;
35
+ priority: AlertPriority;
36
+ driftEvents: DriftEvent[];
37
+ summary: string;
38
+ affectedFrameworks: string[];
39
+ affectedControlCount: number;
40
+ maxSeverity: DriftSeverity;
41
+ overallScoreDelta: number;
42
+ }
43
+ export interface BaselineSnapshot {
44
+ id: string;
45
+ capturedAt: string;
46
+ controls: ControlSnapshot[];
47
+ overallScore: number;
48
+ frameworkScores: Record<string, number>;
49
+ controlCount: number;
50
+ compliantCount: number;
51
+ hash: string;
52
+ }
53
+ export interface DriftDetectionResult {
54
+ snapshotId: string;
55
+ baselineId: string;
56
+ detectedAt: string;
57
+ driftEvents: DriftEvent[];
58
+ driftDetected: boolean;
59
+ overallScoreDelta: number;
60
+ currentScore: number;
61
+ baselineScore: number;
62
+ alerts: DriftAlert[];
63
+ }
64
+ export interface DriftDetectorConfig {
65
+ tenantId: number;
66
+ frameworks: string[];
67
+ driftThresholdPercent?: number;
68
+ scoreDeltaAlertThreshold?: number;
69
+ pollIntervalMs?: number;
70
+ maxBaselineHistory?: number;
71
+ onDrift?: (events: DriftEvent[]) => void;
72
+ onAlert?: (alert: DriftAlert) => void;
73
+ }
74
+ export interface ControlEvaluator {
75
+ evaluateControl(controlId: string, framework: string): Promise<ControlSnapshot>;
76
+ listControls(framework: string): Promise<Array<{
77
+ controlId: string;
78
+ title: string;
79
+ }>>;
80
+ }
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
package/package.json ADDED
@@ -0,0 +1,25 @@
1
+ {
2
+ "name": "@grc-claw/drift-detector",
3
+ "version": "1.0.0",
4
+ "description": "Real-time compliance drift detection — monitors control posture changes, detects evidence gaps, and broadcasts drift alerts",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "main": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./dist/index.js"
13
+ }
14
+ },
15
+ "scripts": {
16
+ "build": "tsc -p tsconfig.json",
17
+ "test": "node --test dist/**/*.test.js 2>/dev/null || true"
18
+ },
19
+ "files": [
20
+ "dist"
21
+ ],
22
+ "publishConfig": {
23
+ "access": "public"
24
+ }
25
+ }