@way_marks/server 0.9.0 → 2.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.
- package/dist/api/server.js +598 -0
- package/dist/approval/manager.js +249 -0
- package/dist/db/database.js +754 -185
- package/dist/escalation/manager.js +205 -0
- package/dist/notifications/service.js +457 -0
- package/dist/policies/engine.js +11 -5
- package/dist/policy/engine.js +420 -0
- package/dist/remediation/recommender.js +309 -0
- package/dist/risk/analyzer.js +453 -0
- package/dist/rollback/blocker.js +222 -0
- package/dist/rollback/manager.js +245 -0
- package/package.json +1 -1
- package/src/ui/index.html +862 -0
- package/dist/approvals/handler.test.js +0 -172
- package/dist/policies/engine.test.js +0 -241
package/dist/policies/engine.js
CHANGED
|
@@ -42,8 +42,13 @@ exports.checkBashAction = checkBashAction;
|
|
|
42
42
|
const fs = __importStar(require("fs"));
|
|
43
43
|
const path = __importStar(require("path"));
|
|
44
44
|
const micromatch_1 = __importDefault(require("micromatch"));
|
|
45
|
-
|
|
46
|
-
|
|
45
|
+
// Evaluate at runtime to support test environment variable changes
|
|
46
|
+
function getProjectRoot() {
|
|
47
|
+
return process.env.WAYMARK_PROJECT_ROOT || process.cwd();
|
|
48
|
+
}
|
|
49
|
+
function getConfigPath() {
|
|
50
|
+
return path.join(getProjectRoot(), 'waymark.config.json');
|
|
51
|
+
}
|
|
47
52
|
const DEFAULT_CONFIG = {
|
|
48
53
|
version: '1',
|
|
49
54
|
policies: {
|
|
@@ -56,7 +61,8 @@ const DEFAULT_CONFIG = {
|
|
|
56
61
|
};
|
|
57
62
|
function loadConfig() {
|
|
58
63
|
try {
|
|
59
|
-
const
|
|
64
|
+
const configPath = getConfigPath();
|
|
65
|
+
const raw = fs.readFileSync(configPath, 'utf8');
|
|
60
66
|
const parsed = JSON.parse(raw);
|
|
61
67
|
// Ensure all policy arrays exist
|
|
62
68
|
if (!parsed.policies)
|
|
@@ -80,7 +86,7 @@ function resolvePattern(pattern) {
|
|
|
80
86
|
// Absolute patterns pass through; relative (./...) resolve from project root
|
|
81
87
|
if (path.isAbsolute(pattern))
|
|
82
88
|
return pattern;
|
|
83
|
-
return path.resolve(
|
|
89
|
+
return path.resolve(getProjectRoot(), pattern);
|
|
84
90
|
}
|
|
85
91
|
function matchesAny(absFilePath, patterns) {
|
|
86
92
|
for (const pattern of patterns) {
|
|
@@ -92,7 +98,7 @@ function matchesAny(absFilePath, patterns) {
|
|
|
92
98
|
return null;
|
|
93
99
|
}
|
|
94
100
|
function checkFileAction(filePath, action, config) {
|
|
95
|
-
const absPath = path.isAbsolute(filePath) ? filePath : path.resolve(
|
|
101
|
+
const absPath = path.isAbsolute(filePath) ? filePath : path.resolve(getProjectRoot(), filePath);
|
|
96
102
|
const { blockedPaths, requireApproval, allowedPaths } = config.policies;
|
|
97
103
|
// 1. Blocked (both read and write)
|
|
98
104
|
const blockedMatch = matchesAny(absPath, blockedPaths);
|
|
@@ -0,0 +1,420 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Policy Engine — Phase 4B
|
|
4
|
+
*
|
|
5
|
+
* Implements policy matching and evaluation for:
|
|
6
|
+
* - Compliance policies (HIPAA, SOC2, PCI-DSS, GDPR)
|
|
7
|
+
* - Operational policies (business hours, action limits)
|
|
8
|
+
* - Security policies (auth changes, patch downgrades)
|
|
9
|
+
* - Data policies (backup protection, schema migration)
|
|
10
|
+
*
|
|
11
|
+
* Evaluates whether actions violate any active policies
|
|
12
|
+
*/
|
|
13
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
14
|
+
exports.evaluatePolicies = evaluatePolicies;
|
|
15
|
+
exports.evaluatePolicy = evaluatePolicy;
|
|
16
|
+
exports.evaluateCondition = evaluateCondition;
|
|
17
|
+
exports.getDefaultCompliancePolicies = getDefaultCompliancePolicies;
|
|
18
|
+
exports.getDefaultOperationalPolicies = getDefaultOperationalPolicies;
|
|
19
|
+
exports.getDefaultSecurityPolicies = getDefaultSecurityPolicies;
|
|
20
|
+
exports.getDefaultDataPolicies = getDefaultDataPolicies;
|
|
21
|
+
exports.createPolicy = createPolicy;
|
|
22
|
+
/**
|
|
23
|
+
* Evaluate actions against all policies
|
|
24
|
+
*/
|
|
25
|
+
function evaluatePolicies(session, actions, policies) {
|
|
26
|
+
const violations = [];
|
|
27
|
+
const log_entries = [];
|
|
28
|
+
// Filter enabled policies
|
|
29
|
+
const enabledPolicies = policies.filter(p => p.enabled);
|
|
30
|
+
for (const policy of enabledPolicies) {
|
|
31
|
+
const policyViolations = evaluatePolicy(session, actions, policy);
|
|
32
|
+
violations.push(...policyViolations);
|
|
33
|
+
if (policyViolations.length > 0) {
|
|
34
|
+
log_entries.push(`[${policy.category.toUpperCase()}] ${policy.name}: ${policyViolations.length} violation(s)`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
const has_blocks = violations.some(v => v.action === 'block');
|
|
38
|
+
const has_warnings = violations.some(v => v.severity === 'warning');
|
|
39
|
+
const requires_approval = violations.some(v => v.action === 'require_approval');
|
|
40
|
+
const requires_remediation = violations.some(v => v.action === 'require_remediation');
|
|
41
|
+
return {
|
|
42
|
+
violations,
|
|
43
|
+
has_blocks,
|
|
44
|
+
has_warnings,
|
|
45
|
+
requires_approval,
|
|
46
|
+
requires_remediation,
|
|
47
|
+
log_entries,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Evaluate actions against a single policy
|
|
52
|
+
*/
|
|
53
|
+
function evaluatePolicy(session, actions, policy) {
|
|
54
|
+
const violations = [];
|
|
55
|
+
const timestamp = new Date().toISOString();
|
|
56
|
+
for (let ruleIndex = 0; ruleIndex < policy.rules.length; ruleIndex++) {
|
|
57
|
+
const rule = policy.rules[ruleIndex];
|
|
58
|
+
// Check if any action matches this rule
|
|
59
|
+
for (const action of actions) {
|
|
60
|
+
if (evaluateCondition(rule.condition, action, session)) {
|
|
61
|
+
violations.push({
|
|
62
|
+
policy_id: policy.policy_id,
|
|
63
|
+
policy_name: policy.name,
|
|
64
|
+
category: policy.category,
|
|
65
|
+
rule_index: ruleIndex,
|
|
66
|
+
action: rule.action,
|
|
67
|
+
severity: rule.severity,
|
|
68
|
+
message: rule.message,
|
|
69
|
+
violated_at: timestamp,
|
|
70
|
+
});
|
|
71
|
+
// Only record once per rule (not per action)
|
|
72
|
+
break;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return violations;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Evaluate a single condition against an action
|
|
80
|
+
*/
|
|
81
|
+
function evaluateCondition(condition, action, session) {
|
|
82
|
+
const value = condition.value;
|
|
83
|
+
const op = condition.operator;
|
|
84
|
+
switch (condition.type) {
|
|
85
|
+
case 'operation_type':
|
|
86
|
+
return evaluateEquals(action.tool_name, value, op, condition.caseSensitive);
|
|
87
|
+
case 'tool_name':
|
|
88
|
+
return evaluateEquals(action.tool_name, value, op, condition.caseSensitive);
|
|
89
|
+
case 'file_pattern':
|
|
90
|
+
return evaluatePattern(action.target || '', value, op, condition.caseSensitive);
|
|
91
|
+
case 'action_count':
|
|
92
|
+
return evaluateNumeric(session.action_count, value, op);
|
|
93
|
+
case 'data_type':
|
|
94
|
+
return evaluateDataType(action, value);
|
|
95
|
+
case 'time_of_day':
|
|
96
|
+
return evaluateTimeOfDay(value);
|
|
97
|
+
case 'error_present':
|
|
98
|
+
return evaluateErrorPresent(action, op === 'equals' ? (value === 'true') : (value !== 'true'));
|
|
99
|
+
default:
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Evaluate equality/contains conditions
|
|
105
|
+
*/
|
|
106
|
+
function evaluateEquals(actual, expected, operator, caseSensitive) {
|
|
107
|
+
const a = caseSensitive ? actual : actual.toLowerCase();
|
|
108
|
+
const normalizeValue = (v) => {
|
|
109
|
+
if (Array.isArray(v)) {
|
|
110
|
+
return v.map(x => (caseSensitive ? String(x) : String(x).toLowerCase()));
|
|
111
|
+
}
|
|
112
|
+
return caseSensitive ? String(v) : String(v).toLowerCase();
|
|
113
|
+
};
|
|
114
|
+
const e = normalizeValue(expected);
|
|
115
|
+
if (operator === 'equals') {
|
|
116
|
+
if (Array.isArray(e)) {
|
|
117
|
+
return e.includes(a);
|
|
118
|
+
}
|
|
119
|
+
return a === e;
|
|
120
|
+
}
|
|
121
|
+
if (operator === 'contains') {
|
|
122
|
+
if (Array.isArray(e)) {
|
|
123
|
+
return e.some(x => a.includes(x));
|
|
124
|
+
}
|
|
125
|
+
return a.includes(String(e));
|
|
126
|
+
}
|
|
127
|
+
if (operator === 'in') {
|
|
128
|
+
if (Array.isArray(e)) {
|
|
129
|
+
return e.includes(a);
|
|
130
|
+
}
|
|
131
|
+
return a === String(e);
|
|
132
|
+
}
|
|
133
|
+
return false;
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Evaluate pattern matching (file paths, regex)
|
|
137
|
+
*/
|
|
138
|
+
function evaluatePattern(actual, pattern, operator, caseSensitive) {
|
|
139
|
+
const patternStr = Array.isArray(pattern) ? pattern[0] : String(pattern);
|
|
140
|
+
if (operator === 'matches_regex') {
|
|
141
|
+
const flags = caseSensitive ? '' : 'i';
|
|
142
|
+
try {
|
|
143
|
+
const regex = new RegExp(patternStr, flags);
|
|
144
|
+
return regex.test(actual);
|
|
145
|
+
}
|
|
146
|
+
catch {
|
|
147
|
+
return false;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
if (operator === 'contains') {
|
|
151
|
+
const a = caseSensitive ? actual : actual.toLowerCase();
|
|
152
|
+
const p = caseSensitive ? patternStr : patternStr.toLowerCase();
|
|
153
|
+
return a.includes(p);
|
|
154
|
+
}
|
|
155
|
+
return false;
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Evaluate numeric comparisons
|
|
159
|
+
*/
|
|
160
|
+
function evaluateNumeric(actual, expected, operator) {
|
|
161
|
+
const expectedNum = typeof expected === 'number' ? expected : parseInt(String(expected), 10);
|
|
162
|
+
if (isNaN(expectedNum))
|
|
163
|
+
return false;
|
|
164
|
+
switch (operator) {
|
|
165
|
+
case 'equals':
|
|
166
|
+
return actual === expectedNum;
|
|
167
|
+
case 'greater_than':
|
|
168
|
+
return actual > expectedNum;
|
|
169
|
+
case 'less_than':
|
|
170
|
+
return actual < expectedNum;
|
|
171
|
+
default:
|
|
172
|
+
return false;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Evaluate data type conditions
|
|
177
|
+
*/
|
|
178
|
+
function evaluateDataType(action, dataType) {
|
|
179
|
+
const typeStr = Array.isArray(dataType) ? dataType[0] : String(dataType).toLowerCase();
|
|
180
|
+
const target = (action.target || '').toLowerCase();
|
|
181
|
+
const typePatterns = {
|
|
182
|
+
sensitive_data: [
|
|
183
|
+
'/password',
|
|
184
|
+
'/api_key',
|
|
185
|
+
'/secret',
|
|
186
|
+
'/token',
|
|
187
|
+
'/credential',
|
|
188
|
+
'/pii',
|
|
189
|
+
'/users',
|
|
190
|
+
'/email',
|
|
191
|
+
'/ssn',
|
|
192
|
+
'/credit_card',
|
|
193
|
+
],
|
|
194
|
+
database: ['.sql', '.db', '/migrations', '/schema', 'database'],
|
|
195
|
+
config: ['.env', 'config.json', 'secrets', '.key', '.pem'],
|
|
196
|
+
authentication: ['/auth', '/login', '/users', '/password', '/oauth'],
|
|
197
|
+
};
|
|
198
|
+
const patterns = typePatterns[typeStr] || [];
|
|
199
|
+
return patterns.some(p => target.includes(p));
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Evaluate time of day conditions
|
|
203
|
+
*/
|
|
204
|
+
function evaluateTimeOfDay(timeRange) {
|
|
205
|
+
const now = new Date();
|
|
206
|
+
const hour = now.getHours();
|
|
207
|
+
const dayOfWeek = now.getDay(); // 0 = Sunday, 6 = Saturday
|
|
208
|
+
// Supported formats: 'business_hours', 'off_hours', 'weekday', 'weekend', 'night'
|
|
209
|
+
switch (timeRange.toLowerCase()) {
|
|
210
|
+
case 'business_hours':
|
|
211
|
+
return hour >= 9 && hour < 17 && dayOfWeek >= 1 && dayOfWeek <= 5;
|
|
212
|
+
case 'off_hours':
|
|
213
|
+
return hour < 9 || hour >= 17 || dayOfWeek === 0 || dayOfWeek === 6;
|
|
214
|
+
case 'weekday':
|
|
215
|
+
return dayOfWeek >= 1 && dayOfWeek <= 5;
|
|
216
|
+
case 'weekend':
|
|
217
|
+
return dayOfWeek === 0 || dayOfWeek === 6;
|
|
218
|
+
case 'night':
|
|
219
|
+
return hour >= 22 || hour < 6;
|
|
220
|
+
default:
|
|
221
|
+
return false;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* Evaluate error presence conditions
|
|
226
|
+
*/
|
|
227
|
+
function evaluateErrorPresent(action, shouldHaveError) {
|
|
228
|
+
const hasError = action.status === 'error';
|
|
229
|
+
return hasError === shouldHaveError;
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* Get default compliance policies
|
|
233
|
+
*/
|
|
234
|
+
function getDefaultCompliancePolicies() {
|
|
235
|
+
return [
|
|
236
|
+
{
|
|
237
|
+
policy_id: 'compliance-hipaa',
|
|
238
|
+
name: 'HIPAA Compliance',
|
|
239
|
+
description: 'Protect health information data',
|
|
240
|
+
category: 'compliance',
|
|
241
|
+
enabled: false, // Opt-in
|
|
242
|
+
created_at: new Date().toISOString(),
|
|
243
|
+
created_by: 'system',
|
|
244
|
+
rules: [
|
|
245
|
+
{
|
|
246
|
+
condition: {
|
|
247
|
+
type: 'data_type',
|
|
248
|
+
operator: 'equals',
|
|
249
|
+
value: 'sensitive_data',
|
|
250
|
+
},
|
|
251
|
+
action: 'require_approval',
|
|
252
|
+
severity: 'error',
|
|
253
|
+
message: 'HIPAA: Protected health information requires additional approval',
|
|
254
|
+
},
|
|
255
|
+
{
|
|
256
|
+
condition: {
|
|
257
|
+
type: 'operation_type',
|
|
258
|
+
operator: 'equals',
|
|
259
|
+
value: 'delete_file',
|
|
260
|
+
},
|
|
261
|
+
action: 'require_approval',
|
|
262
|
+
severity: 'error',
|
|
263
|
+
message: 'HIPAA: Deletion of records requires audit trail verification',
|
|
264
|
+
},
|
|
265
|
+
],
|
|
266
|
+
},
|
|
267
|
+
{
|
|
268
|
+
policy_id: 'compliance-soc2',
|
|
269
|
+
name: 'SOC2 Compliance',
|
|
270
|
+
description: 'Ensure system security and availability',
|
|
271
|
+
category: 'compliance',
|
|
272
|
+
enabled: false, // Opt-in
|
|
273
|
+
created_at: new Date().toISOString(),
|
|
274
|
+
created_by: 'system',
|
|
275
|
+
rules: [
|
|
276
|
+
{
|
|
277
|
+
condition: {
|
|
278
|
+
type: 'time_of_day',
|
|
279
|
+
operator: 'equals',
|
|
280
|
+
value: 'off_hours',
|
|
281
|
+
},
|
|
282
|
+
action: 'require_approval',
|
|
283
|
+
severity: 'warning',
|
|
284
|
+
message: 'SOC2: Production changes outside business hours require approval',
|
|
285
|
+
},
|
|
286
|
+
{
|
|
287
|
+
condition: {
|
|
288
|
+
type: 'operation_type',
|
|
289
|
+
operator: 'equals',
|
|
290
|
+
value: 'bash',
|
|
291
|
+
},
|
|
292
|
+
action: 'require_approval',
|
|
293
|
+
severity: 'warning',
|
|
294
|
+
message: 'SOC2: System commands must be logged and approved',
|
|
295
|
+
},
|
|
296
|
+
],
|
|
297
|
+
},
|
|
298
|
+
];
|
|
299
|
+
}
|
|
300
|
+
/**
|
|
301
|
+
* Get default operational policies
|
|
302
|
+
*/
|
|
303
|
+
function getDefaultOperationalPolicies() {
|
|
304
|
+
return [
|
|
305
|
+
{
|
|
306
|
+
policy_id: 'ops-scale-limit',
|
|
307
|
+
name: 'Rollback Scale Limit',
|
|
308
|
+
description: 'Prevent rollback of too many operations at once',
|
|
309
|
+
category: 'operational',
|
|
310
|
+
enabled: true,
|
|
311
|
+
created_at: new Date().toISOString(),
|
|
312
|
+
created_by: 'system',
|
|
313
|
+
rules: [
|
|
314
|
+
{
|
|
315
|
+
condition: {
|
|
316
|
+
type: 'action_count',
|
|
317
|
+
operator: 'greater_than',
|
|
318
|
+
value: 50,
|
|
319
|
+
},
|
|
320
|
+
action: 'require_remediation',
|
|
321
|
+
severity: 'warning',
|
|
322
|
+
message: 'Cannot rollback more than 50 actions at once; consider partial rollback',
|
|
323
|
+
},
|
|
324
|
+
{
|
|
325
|
+
condition: {
|
|
326
|
+
type: 'action_count',
|
|
327
|
+
operator: 'greater_than',
|
|
328
|
+
value: 100,
|
|
329
|
+
},
|
|
330
|
+
action: 'block',
|
|
331
|
+
severity: 'error',
|
|
332
|
+
message: 'Cannot rollback more than 100 actions; use staged rollback approach',
|
|
333
|
+
},
|
|
334
|
+
],
|
|
335
|
+
},
|
|
336
|
+
];
|
|
337
|
+
}
|
|
338
|
+
/**
|
|
339
|
+
* Get default security policies
|
|
340
|
+
*/
|
|
341
|
+
function getDefaultSecurityPolicies() {
|
|
342
|
+
return [
|
|
343
|
+
{
|
|
344
|
+
policy_id: 'sec-auth-changes',
|
|
345
|
+
name: 'Authentication Changes Protection',
|
|
346
|
+
description: 'Require approval for authentication modifications',
|
|
347
|
+
category: 'security',
|
|
348
|
+
enabled: true,
|
|
349
|
+
created_at: new Date().toISOString(),
|
|
350
|
+
created_by: 'system',
|
|
351
|
+
rules: [
|
|
352
|
+
{
|
|
353
|
+
condition: {
|
|
354
|
+
type: 'file_pattern',
|
|
355
|
+
operator: 'matches_regex',
|
|
356
|
+
value: '/(auth|login|password|oauth|jwt)',
|
|
357
|
+
caseSensitive: false,
|
|
358
|
+
},
|
|
359
|
+
action: 'require_approval',
|
|
360
|
+
severity: 'error',
|
|
361
|
+
message: 'Security: Authentication changes require security team approval',
|
|
362
|
+
},
|
|
363
|
+
],
|
|
364
|
+
},
|
|
365
|
+
];
|
|
366
|
+
}
|
|
367
|
+
/**
|
|
368
|
+
* Get default data policies
|
|
369
|
+
*/
|
|
370
|
+
function getDefaultDataPolicies() {
|
|
371
|
+
return [
|
|
372
|
+
{
|
|
373
|
+
policy_id: 'data-schema-changes',
|
|
374
|
+
name: 'Schema Change Protection',
|
|
375
|
+
description: 'Require DBA approval for database schema changes',
|
|
376
|
+
category: 'data',
|
|
377
|
+
enabled: true,
|
|
378
|
+
created_at: new Date().toISOString(),
|
|
379
|
+
created_by: 'system',
|
|
380
|
+
rules: [
|
|
381
|
+
{
|
|
382
|
+
condition: {
|
|
383
|
+
type: 'file_pattern',
|
|
384
|
+
operator: 'matches_regex',
|
|
385
|
+
value: '/(migrations|schema|\.sql)$',
|
|
386
|
+
caseSensitive: false,
|
|
387
|
+
},
|
|
388
|
+
action: 'require_approval',
|
|
389
|
+
severity: 'error',
|
|
390
|
+
message: 'Data: Database schema changes require DBA review',
|
|
391
|
+
},
|
|
392
|
+
],
|
|
393
|
+
},
|
|
394
|
+
];
|
|
395
|
+
}
|
|
396
|
+
/**
|
|
397
|
+
* Create a custom policy
|
|
398
|
+
*/
|
|
399
|
+
function createPolicy(name, description, category, rules, createdBy = 'system') {
|
|
400
|
+
return {
|
|
401
|
+
policy_id: `policy-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
|
|
402
|
+
name,
|
|
403
|
+
description,
|
|
404
|
+
category,
|
|
405
|
+
rules,
|
|
406
|
+
enabled: true,
|
|
407
|
+
created_at: new Date().toISOString(),
|
|
408
|
+
created_by: createdBy,
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
exports.default = {
|
|
412
|
+
evaluatePolicies,
|
|
413
|
+
evaluatePolicy,
|
|
414
|
+
evaluateCondition,
|
|
415
|
+
getDefaultCompliancePolicies,
|
|
416
|
+
getDefaultOperationalPolicies,
|
|
417
|
+
getDefaultSecurityPolicies,
|
|
418
|
+
getDefaultDataPolicies,
|
|
419
|
+
createPolicy,
|
|
420
|
+
};
|