@eddacraft/anvil-kindling-integration 0.1.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.
- package/LICENSE +14 -0
- package/README.md +542 -0
- package/dist/adapter.d.ts +49 -0
- package/dist/adapter.d.ts.map +1 -0
- package/dist/adapter.js +100 -0
- package/dist/config.d.ts +89 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +173 -0
- package/dist/emitters/action-emitter.d.ts +40 -0
- package/dist/emitters/action-emitter.d.ts.map +1 -0
- package/dist/emitters/action-emitter.js +52 -0
- package/dist/emitters/constraint-emitter.d.ts +32 -0
- package/dist/emitters/constraint-emitter.d.ts.map +1 -0
- package/dist/emitters/constraint-emitter.js +41 -0
- package/dist/emitters/error-emitter.d.ts +33 -0
- package/dist/emitters/error-emitter.d.ts.map +1 -0
- package/dist/emitters/error-emitter.js +50 -0
- package/dist/emitters/gate-emitter.d.ts +37 -0
- package/dist/emitters/gate-emitter.d.ts.map +1 -0
- package/dist/emitters/gate-emitter.js +53 -0
- package/dist/emitters/human-input-emitter.d.ts +30 -0
- package/dist/emitters/human-input-emitter.d.ts.map +1 -0
- package/dist/emitters/human-input-emitter.js +38 -0
- package/dist/emitters/index.d.ts +13 -0
- package/dist/emitters/index.d.ts.map +1 -0
- package/dist/emitters/index.js +19 -0
- package/dist/emitters/plan-emitter.d.ts +75 -0
- package/dist/emitters/plan-emitter.d.ts.map +1 -0
- package/dist/emitters/plan-emitter.js +116 -0
- package/dist/emitters/session-emitter.d.ts +57 -0
- package/dist/emitters/session-emitter.d.ts.map +1 -0
- package/dist/emitters/session-emitter.js +80 -0
- package/dist/index.d.ts +40 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +111 -0
- package/dist/kindling-service.d.ts +122 -0
- package/dist/kindling-service.d.ts.map +1 -0
- package/dist/kindling-service.js +203 -0
- package/dist/observation-contract.d.ts +561 -0
- package/dist/observation-contract.d.ts.map +1 -0
- package/dist/observation-contract.js +391 -0
- package/dist/query-contract.d.ts +463 -0
- package/dist/query-contract.d.ts.map +1 -0
- package/dist/query-contract.js +314 -0
- package/dist/query-limits.d.ts +40 -0
- package/dist/query-limits.d.ts.map +1 -0
- package/dist/query-limits.js +79 -0
- package/dist/query-service.d.ts +109 -0
- package/dist/query-service.d.ts.map +1 -0
- package/dist/query-service.js +140 -0
- package/dist/retention.d.ts +79 -0
- package/dist/retention.d.ts.map +1 -0
- package/dist/retention.js +81 -0
- package/dist/sensitive-data-validator.d.ts +47 -0
- package/dist/sensitive-data-validator.d.ts.map +1 -0
- package/dist/sensitive-data-validator.js +135 -0
- package/dist/status.d.ts +104 -0
- package/dist/status.d.ts.map +1 -0
- package/dist/status.js +136 -0
- package/dist/utils/debug.d.ts +9 -0
- package/dist/utils/debug.d.ts.map +1 -0
- package/dist/utils/debug.js +55 -0
- package/package.json +114 -0
- package/src/adapter.ts +117 -0
- package/src/config.ts +202 -0
- package/src/emitters/action-emitter.ts +90 -0
- package/src/emitters/constraint-emitter.ts +73 -0
- package/src/emitters/error-emitter.ts +86 -0
- package/src/emitters/gate-emitter.ts +87 -0
- package/src/emitters/human-input-emitter.ts +71 -0
- package/src/emitters/index.ts +40 -0
- package/src/emitters/plan-emitter.ts +183 -0
- package/src/emitters/session-emitter.ts +131 -0
- package/src/index.ts +254 -0
- package/src/kindling-service.ts +272 -0
- package/src/malicious-ai.test.ts +949 -0
- package/src/observation-contract.ts +500 -0
- package/src/query-contract.ts +389 -0
- package/src/query-limits.ts +106 -0
- package/src/query-service.ts +217 -0
- package/src/retention.ts +153 -0
- package/src/sensitive-data-validator.ts +167 -0
- package/src/status.ts +221 -0
- package/src/utils/debug.ts +65 -0
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Kindling Query Service (KINDLING-009)
|
|
3
|
+
*
|
|
4
|
+
* High-level query interface that wraps KindlingService.query() with
|
|
5
|
+
* convenience methods for each query scope. Applies query limit enforcement
|
|
6
|
+
* before returning results.
|
|
7
|
+
*
|
|
8
|
+
* This is the primary read API for Anvil CLI commands and tooling.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { KindlingService } from './kindling-service.js';
|
|
12
|
+
import type {
|
|
13
|
+
QueryResponse,
|
|
14
|
+
SessionQuery,
|
|
15
|
+
PlanQuery,
|
|
16
|
+
GateQuery,
|
|
17
|
+
ActionQuery,
|
|
18
|
+
ResultShape,
|
|
19
|
+
OutputFormat,
|
|
20
|
+
} from './query-contract.js';
|
|
21
|
+
import { enforceQueryLimits, limitsFromConfig } from './query-limits.js';
|
|
22
|
+
import { createDebugger } from './utils/debug.js';
|
|
23
|
+
|
|
24
|
+
const debug = createDebugger('kindling');
|
|
25
|
+
|
|
26
|
+
// =============================================================================
|
|
27
|
+
// Query Options
|
|
28
|
+
// =============================================================================
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Common options for all query methods
|
|
32
|
+
*/
|
|
33
|
+
export interface QueryOptions {
|
|
34
|
+
/** Result structure (default: 'list') */
|
|
35
|
+
shape?: ResultShape;
|
|
36
|
+
/** Output format (default: 'json') */
|
|
37
|
+
format?: OutputFormat;
|
|
38
|
+
/** Include observations after this time */
|
|
39
|
+
time_after?: string;
|
|
40
|
+
/** Include observations before this time */
|
|
41
|
+
time_before?: string;
|
|
42
|
+
/** Maximum observations to return (capped by config) */
|
|
43
|
+
max_results?: number;
|
|
44
|
+
/** Maximum total payload size in bytes (capped by config) */
|
|
45
|
+
max_payload_bytes?: number;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Options specific to session queries
|
|
50
|
+
*/
|
|
51
|
+
export interface SessionQueryOptions extends QueryOptions {
|
|
52
|
+
/** Filter to specific phases */
|
|
53
|
+
include_phases?: Array<'plan' | 'gate' | 'action' | 'outcome' | 'error'>;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Options specific to plan queries
|
|
58
|
+
*/
|
|
59
|
+
export interface PlanQueryOptions extends QueryOptions {
|
|
60
|
+
/** Include linked execution run IDs (default: true) */
|
|
61
|
+
include_executions?: boolean;
|
|
62
|
+
/** Include plan version history (default: true) */
|
|
63
|
+
include_versions?: boolean;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Options specific to action queries
|
|
68
|
+
*/
|
|
69
|
+
export interface ActionQueryOptions extends QueryOptions {
|
|
70
|
+
/** Include approval chain (default: true) */
|
|
71
|
+
include_approval_chain?: boolean;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// =============================================================================
|
|
75
|
+
// Query Service
|
|
76
|
+
// =============================================================================
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* High-level query service for Kindling observations.
|
|
80
|
+
*
|
|
81
|
+
* Provides typed convenience methods for each query scope and enforces
|
|
82
|
+
* query limits from the service configuration.
|
|
83
|
+
*/
|
|
84
|
+
export class KindlingQueryService {
|
|
85
|
+
private readonly service: KindlingService;
|
|
86
|
+
|
|
87
|
+
constructor(service: KindlingService) {
|
|
88
|
+
this.service = service;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Query: "What happened in this run?"
|
|
93
|
+
*
|
|
94
|
+
* Returns ordered observations for a specific session, optionally
|
|
95
|
+
* filtered by phase.
|
|
96
|
+
*
|
|
97
|
+
* @param sessionId - Session/run ID
|
|
98
|
+
* @param options - Query options
|
|
99
|
+
* @returns Query response with session observations
|
|
100
|
+
*/
|
|
101
|
+
async querySession(sessionId: string, options: SessionQueryOptions = {}): Promise<QueryResponse> {
|
|
102
|
+
debug('querySession', { sessionId, shape: options.shape });
|
|
103
|
+
const request: SessionQuery = {
|
|
104
|
+
scope: 'session',
|
|
105
|
+
session_id: sessionId,
|
|
106
|
+
shape: options.shape ?? 'timeline',
|
|
107
|
+
format: options.format ?? 'json',
|
|
108
|
+
time_after: options.time_after,
|
|
109
|
+
time_before: options.time_before,
|
|
110
|
+
max_results: options.max_results ?? this.service.configuration.query_limits.max_results,
|
|
111
|
+
max_payload_bytes:
|
|
112
|
+
options.max_payload_bytes ?? this.service.configuration.query_limits.max_payload_bytes,
|
|
113
|
+
include_phases: options.include_phases,
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
const response = await this.service.query(request);
|
|
117
|
+
return this.applyLimits(response);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Query: "What happened because of this plan?"
|
|
122
|
+
*
|
|
123
|
+
* Returns plan metadata, versions, and linked executions.
|
|
124
|
+
* This is the only cross-session read allowed.
|
|
125
|
+
*
|
|
126
|
+
* @param planId - Plan ID
|
|
127
|
+
* @param options - Query options
|
|
128
|
+
* @returns Query response with plan observations
|
|
129
|
+
*/
|
|
130
|
+
async queryPlan(planId: string, options: PlanQueryOptions = {}): Promise<QueryResponse> {
|
|
131
|
+
debug('queryPlan', { planId, shape: options.shape });
|
|
132
|
+
const request: PlanQuery = {
|
|
133
|
+
scope: 'plan',
|
|
134
|
+
plan_id: planId,
|
|
135
|
+
shape: options.shape ?? 'entity',
|
|
136
|
+
format: options.format ?? 'json',
|
|
137
|
+
time_after: options.time_after,
|
|
138
|
+
time_before: options.time_before,
|
|
139
|
+
max_results: options.max_results ?? this.service.configuration.query_limits.max_results,
|
|
140
|
+
max_payload_bytes:
|
|
141
|
+
options.max_payload_bytes ?? this.service.configuration.query_limits.max_payload_bytes,
|
|
142
|
+
include_executions: options.include_executions ?? true,
|
|
143
|
+
include_versions: options.include_versions ?? true,
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
const response = await this.service.query(request);
|
|
147
|
+
return this.applyLimits(response);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Query: "Why did this gate pass/fail?"
|
|
152
|
+
*
|
|
153
|
+
* Returns gate evaluation details with rule IDs, inputs, and outcomes.
|
|
154
|
+
*
|
|
155
|
+
* @param gateEvalId - Gate evaluation ID
|
|
156
|
+
* @param options - Query options
|
|
157
|
+
* @returns Query response with gate observations
|
|
158
|
+
*/
|
|
159
|
+
async queryGate(gateEvalId: string, options: QueryOptions = {}): Promise<QueryResponse> {
|
|
160
|
+
debug('queryGate', { gateEvalId });
|
|
161
|
+
const request: GateQuery = {
|
|
162
|
+
scope: 'gate',
|
|
163
|
+
gate_eval_id: gateEvalId,
|
|
164
|
+
shape: options.shape ?? 'entity',
|
|
165
|
+
format: options.format ?? 'json',
|
|
166
|
+
time_after: options.time_after,
|
|
167
|
+
time_before: options.time_before,
|
|
168
|
+
max_results: options.max_results ?? this.service.configuration.query_limits.max_results,
|
|
169
|
+
max_payload_bytes:
|
|
170
|
+
options.max_payload_bytes ?? this.service.configuration.query_limits.max_payload_bytes,
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
const response = await this.service.query(request);
|
|
174
|
+
return this.applyLimits(response);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Query: "What exactly did this action do?"
|
|
179
|
+
*
|
|
180
|
+
* Returns action execution details with redacted command, environment,
|
|
181
|
+
* and linked governance.
|
|
182
|
+
*
|
|
183
|
+
* @param actionId - Action ID
|
|
184
|
+
* @param options - Query options
|
|
185
|
+
* @returns Query response with action observations
|
|
186
|
+
*/
|
|
187
|
+
async queryAction(actionId: string, options: ActionQueryOptions = {}): Promise<QueryResponse> {
|
|
188
|
+
debug('queryAction', { actionId });
|
|
189
|
+
const request: ActionQuery = {
|
|
190
|
+
scope: 'action',
|
|
191
|
+
action_id: actionId,
|
|
192
|
+
shape: options.shape ?? 'entity',
|
|
193
|
+
format: options.format ?? 'json',
|
|
194
|
+
time_after: options.time_after,
|
|
195
|
+
time_before: options.time_before,
|
|
196
|
+
max_results: options.max_results ?? this.service.configuration.query_limits.max_results,
|
|
197
|
+
max_payload_bytes:
|
|
198
|
+
options.max_payload_bytes ?? this.service.configuration.query_limits.max_payload_bytes,
|
|
199
|
+
include_approval_chain: options.include_approval_chain ?? true,
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
const response = await this.service.query(request);
|
|
203
|
+
return this.applyLimits(response);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// ===========================================================================
|
|
207
|
+
// Private
|
|
208
|
+
// ===========================================================================
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Apply query limits from the service configuration as a defense-in-depth layer.
|
|
212
|
+
*/
|
|
213
|
+
private applyLimits(response: QueryResponse): QueryResponse {
|
|
214
|
+
const limits = limitsFromConfig(this.service.configuration.query_limits);
|
|
215
|
+
return enforceQueryLimits(response, limits);
|
|
216
|
+
}
|
|
217
|
+
}
|
package/src/retention.ts
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Retention Management (KINDLING-016)
|
|
3
|
+
*
|
|
4
|
+
* Handles pruning of old observations and storage statistics.
|
|
5
|
+
* Works against the abstract IKindlingStore interface.
|
|
6
|
+
*
|
|
7
|
+
* Since the abstract store does not expose a direct "delete older than" method,
|
|
8
|
+
* retention is implemented via a dedicated IRetentionCapableStore interface
|
|
9
|
+
* that concrete stores can optionally implement.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type { KindlingConfig } from './config.js';
|
|
13
|
+
|
|
14
|
+
// =============================================================================
|
|
15
|
+
// Retention Store Interface
|
|
16
|
+
// =============================================================================
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Extended store interface for stores that support retention operations.
|
|
20
|
+
*
|
|
21
|
+
* Not all stores need to implement this. The NoOpKindlingStore does not.
|
|
22
|
+
* Concrete SQLite or database-backed stores should implement this interface.
|
|
23
|
+
*/
|
|
24
|
+
export interface IRetentionCapableStore {
|
|
25
|
+
/**
|
|
26
|
+
* Delete all observations with timestamps older than the given ISO8601 date.
|
|
27
|
+
*
|
|
28
|
+
* @param olderThan - ISO8601 datetime cutoff
|
|
29
|
+
* @returns Number of observations deleted
|
|
30
|
+
*/
|
|
31
|
+
deleteObservationsOlderThan(olderThan: string): Promise<number>;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Get storage statistics.
|
|
35
|
+
*
|
|
36
|
+
* @returns Observation count and estimated storage size in bytes
|
|
37
|
+
*/
|
|
38
|
+
getStats(): Promise<StorageStats>;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Storage statistics
|
|
43
|
+
*/
|
|
44
|
+
export interface StorageStats {
|
|
45
|
+
/** Total number of observations in the store */
|
|
46
|
+
observation_count: number;
|
|
47
|
+
/** Estimated storage size in bytes */
|
|
48
|
+
estimated_size_bytes: number;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// =============================================================================
|
|
52
|
+
// Type Guard
|
|
53
|
+
// =============================================================================
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Check if a store supports retention operations.
|
|
57
|
+
*/
|
|
58
|
+
export function isRetentionCapable(store: unknown): store is IRetentionCapableStore {
|
|
59
|
+
if (store === null || typeof store !== 'object') {
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
const candidate = store as Record<string, unknown>;
|
|
63
|
+
return (
|
|
64
|
+
typeof candidate['deleteObservationsOlderThan'] === 'function' &&
|
|
65
|
+
typeof candidate['getStats'] === 'function'
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// =============================================================================
|
|
70
|
+
// Pruning
|
|
71
|
+
// =============================================================================
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Result of a pruning operation
|
|
75
|
+
*/
|
|
76
|
+
export interface PruneResult {
|
|
77
|
+
/** Whether pruning was actually performed */
|
|
78
|
+
pruned: boolean;
|
|
79
|
+
/** Number of observations deleted (0 if not pruned) */
|
|
80
|
+
deleted_count: number;
|
|
81
|
+
/** The cutoff date used */
|
|
82
|
+
cutoff_date: string;
|
|
83
|
+
/** Reason if pruning was skipped */
|
|
84
|
+
skip_reason?: string;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Prune observations older than the configured retention period.
|
|
89
|
+
*
|
|
90
|
+
* If the store does not implement IRetentionCapableStore, this is a no-op
|
|
91
|
+
* that returns a skip result.
|
|
92
|
+
*
|
|
93
|
+
* @param store - The store to prune (must implement IRetentionCapableStore)
|
|
94
|
+
* @param config - Kindling configuration with retention settings
|
|
95
|
+
* @returns Prune result
|
|
96
|
+
*/
|
|
97
|
+
export async function pruneOldObservations(
|
|
98
|
+
store: unknown,
|
|
99
|
+
config: KindlingConfig
|
|
100
|
+
): Promise<PruneResult> {
|
|
101
|
+
if (!config.enabled) {
|
|
102
|
+
return {
|
|
103
|
+
pruned: false,
|
|
104
|
+
deleted_count: 0,
|
|
105
|
+
cutoff_date: '',
|
|
106
|
+
skip_reason: 'Kindling is disabled',
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (!isRetentionCapable(store)) {
|
|
111
|
+
return {
|
|
112
|
+
pruned: false,
|
|
113
|
+
deleted_count: 0,
|
|
114
|
+
cutoff_date: '',
|
|
115
|
+
skip_reason: 'Store does not support retention operations',
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const cutoffDate = new Date();
|
|
120
|
+
cutoffDate.setDate(cutoffDate.getDate() - config.retention.days);
|
|
121
|
+
const cutoffIso = cutoffDate.toISOString();
|
|
122
|
+
|
|
123
|
+
const deletedCount = await store.deleteObservationsOlderThan(cutoffIso);
|
|
124
|
+
|
|
125
|
+
return {
|
|
126
|
+
pruned: true,
|
|
127
|
+
deleted_count: deletedCount,
|
|
128
|
+
cutoff_date: cutoffIso,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// =============================================================================
|
|
133
|
+
// Statistics
|
|
134
|
+
// =============================================================================
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Get storage statistics from the store.
|
|
138
|
+
*
|
|
139
|
+
* If the store does not implement IRetentionCapableStore, returns zero values.
|
|
140
|
+
*
|
|
141
|
+
* @param store - The store to query
|
|
142
|
+
* @returns Storage statistics
|
|
143
|
+
*/
|
|
144
|
+
export async function getStorageStats(store: unknown): Promise<StorageStats> {
|
|
145
|
+
if (!isRetentionCapable(store)) {
|
|
146
|
+
return {
|
|
147
|
+
observation_count: 0,
|
|
148
|
+
estimated_size_bytes: 0,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return store.getStats();
|
|
153
|
+
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sensitive Data Validation (KINDLING-015)
|
|
3
|
+
*
|
|
4
|
+
* Validates and redacts sensitive data from observations before they
|
|
5
|
+
* are persisted to Kindling. This is a defense-in-depth layer on top of
|
|
6
|
+
* the `containsSensitiveData` check in observation-contract.ts.
|
|
7
|
+
*
|
|
8
|
+
* Patterns detected:
|
|
9
|
+
* - API keys (sk-*, ghp_*, AKIA*)
|
|
10
|
+
* - Long hex tokens (40+ characters)
|
|
11
|
+
* - Email addresses
|
|
12
|
+
* - Password-like values
|
|
13
|
+
*
|
|
14
|
+
* @see observation-contract.ts for the base containsSensitiveData utility
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { containsSensitiveData, type Observation } from './observation-contract.js';
|
|
18
|
+
import { createDebugger } from './utils/debug.js';
|
|
19
|
+
|
|
20
|
+
const debug = createDebugger('kindling');
|
|
21
|
+
|
|
22
|
+
// =============================================================================
|
|
23
|
+
// Sensitive Patterns
|
|
24
|
+
// =============================================================================
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Known sensitive value patterns and their replacement strings
|
|
28
|
+
*/
|
|
29
|
+
const SENSITIVE_PATTERNS: ReadonlyArray<{
|
|
30
|
+
name: string;
|
|
31
|
+
pattern: RegExp;
|
|
32
|
+
replacement: string;
|
|
33
|
+
}> = [
|
|
34
|
+
{
|
|
35
|
+
name: 'openai-api-key',
|
|
36
|
+
pattern: /sk-[a-zA-Z0-9]{20,}/g,
|
|
37
|
+
replacement: '[REDACTED:api-key]',
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
name: 'github-pat',
|
|
41
|
+
pattern: /ghp_[a-zA-Z0-9]{36,}/g,
|
|
42
|
+
replacement: '[REDACTED:github-token]',
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
name: 'github-fine-grained-pat',
|
|
46
|
+
pattern: /github_pat_[a-zA-Z0-9_]{36,}/g,
|
|
47
|
+
replacement: '[REDACTED:github-token]',
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
name: 'aws-access-key',
|
|
51
|
+
pattern: /AKIA[0-9A-Z]{16}/g,
|
|
52
|
+
replacement: '[REDACTED:aws-key]',
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
name: 'aws-secret-key',
|
|
56
|
+
pattern: /(?<=aws_secret_access_key\s*[=:]\s*)[^\s"',]+/gi,
|
|
57
|
+
replacement: '[REDACTED:aws-secret]',
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
name: 'hex-token',
|
|
61
|
+
pattern: /\b[0-9a-fA-F]{40,}\b/g,
|
|
62
|
+
replacement: '[REDACTED:token]',
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
name: 'email',
|
|
66
|
+
pattern: /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g,
|
|
67
|
+
replacement: '[REDACTED:email]',
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
name: 'password-value',
|
|
71
|
+
pattern: /(?<=(?:password|passwd|pwd)\s*[=:]\s*["']?)[^\s"',]+/gi,
|
|
72
|
+
replacement: '[REDACTED:password]',
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
name: 'bearer-token',
|
|
76
|
+
pattern: /(?<=Bearer\s+)[a-zA-Z0-9._~+/=-]{20,}/gi,
|
|
77
|
+
replacement: '[REDACTED:bearer-token]',
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
name: 'npm-token',
|
|
81
|
+
pattern: /npm_[a-zA-Z0-9]{36,}/g,
|
|
82
|
+
replacement: '[REDACTED:npm-token]',
|
|
83
|
+
},
|
|
84
|
+
];
|
|
85
|
+
|
|
86
|
+
// =============================================================================
|
|
87
|
+
// Validation
|
|
88
|
+
// =============================================================================
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Result of sensitive data validation
|
|
92
|
+
*/
|
|
93
|
+
export interface SensitiveDataValidationResult {
|
|
94
|
+
/** Whether any sensitive data was detected */
|
|
95
|
+
hasSensitiveData: boolean;
|
|
96
|
+
/** Detailed issues found */
|
|
97
|
+
issues: string[];
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Validate that an observation does not contain sensitive data.
|
|
102
|
+
*
|
|
103
|
+
* Uses the contract-level `containsSensitiveData` check plus additional
|
|
104
|
+
* pattern matching for known credential formats.
|
|
105
|
+
*
|
|
106
|
+
* @param observation - The observation to validate
|
|
107
|
+
* @returns Validation result with issues if sensitive data found
|
|
108
|
+
*/
|
|
109
|
+
export function validateNoSensitiveData(observation: Observation): SensitiveDataValidationResult {
|
|
110
|
+
const issues: string[] = [];
|
|
111
|
+
|
|
112
|
+
// Run the contract-level check first
|
|
113
|
+
const contractCheck = containsSensitiveData(observation);
|
|
114
|
+
if (contractCheck.hasSensitiveData) {
|
|
115
|
+
issues.push(...contractCheck.issues);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Run additional pattern checks against the serialized payload
|
|
119
|
+
const serialized = JSON.stringify(observation);
|
|
120
|
+
|
|
121
|
+
for (const { name, pattern } of SENSITIVE_PATTERNS) {
|
|
122
|
+
// Reset lastIndex for global regex patterns
|
|
123
|
+
pattern.lastIndex = 0;
|
|
124
|
+
if (pattern.test(serialized)) {
|
|
125
|
+
issues.push(`Sensitive pattern detected: ${name}`);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (issues.length > 0) {
|
|
130
|
+
debug('sensitive data detected in observation', {
|
|
131
|
+
issueCount: issues.length,
|
|
132
|
+
patterns: issues,
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return {
|
|
137
|
+
hasSensitiveData: issues.length > 0,
|
|
138
|
+
issues,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// =============================================================================
|
|
143
|
+
// Redaction
|
|
144
|
+
// =============================================================================
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Deep-clone an observation and redact all known sensitive patterns from
|
|
148
|
+
* string values. This operates on the JSON serialization to catch values
|
|
149
|
+
* regardless of nesting depth.
|
|
150
|
+
*
|
|
151
|
+
* The returned observation is safe to persist.
|
|
152
|
+
*
|
|
153
|
+
* @param observation - The observation to redact
|
|
154
|
+
* @returns A new observation with sensitive values replaced
|
|
155
|
+
*/
|
|
156
|
+
export function redactSensitiveFields(observation: Observation): Observation {
|
|
157
|
+
debug('redacting sensitive fields from observation', { kind: observation.kind });
|
|
158
|
+
let serialized = JSON.stringify(observation);
|
|
159
|
+
|
|
160
|
+
for (const { pattern, replacement } of SENSITIVE_PATTERNS) {
|
|
161
|
+
// Reset lastIndex for global regex patterns
|
|
162
|
+
pattern.lastIndex = 0;
|
|
163
|
+
serialized = serialized.replace(pattern, replacement);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return JSON.parse(serialized) as Observation;
|
|
167
|
+
}
|