shield-harness 0.3.0 → 0.5.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/.claude/hooks/lib/automode-detect.js +184 -0
- package/.claude/hooks/lib/policy-drift.js +322 -0
- package/.claude/hooks/sh-config-guard.js +184 -2
- package/.claude/hooks/sh-session-start.js +83 -0
- package/.claude/permissions-spec.json +16 -1
- package/.claude/policies/openshell-generated.yaml +105 -0
- package/README.ja.md +98 -33
- package/README.md +97 -32
- package/package.json +5 -2
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// automode-detect.js — Claude Code Auto Mode detection & danger assessment
|
|
3
|
+
// Spec: Phase 7 ADR-038 (.reference/PHASE7_8_AUTO_MODE_SELF_EVOLUTION_PLAN.md)
|
|
4
|
+
// Purpose: Detect Auto Mode configuration and classify danger level
|
|
5
|
+
"use strict";
|
|
6
|
+
|
|
7
|
+
const fs = require("fs");
|
|
8
|
+
const path = require("path");
|
|
9
|
+
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
// Constants
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
|
|
14
|
+
const SETTINGS_LOCAL = path.join(".claude", "settings.local.json");
|
|
15
|
+
const SETTINGS_MAIN = path.join(".claude", "settings.json");
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Protections lost when autoMode.soft_deny is configured.
|
|
19
|
+
* A single soft_deny entry disables ALL classifier default protections.
|
|
20
|
+
*/
|
|
21
|
+
const LOST_PROTECTIONS = [
|
|
22
|
+
"Default force-push prevention (git push --force)",
|
|
23
|
+
"Default destructive command blocking (rm -rf, format)",
|
|
24
|
+
"Default credential access prevention (~/.ssh, ~/.aws)",
|
|
25
|
+
"Default network isolation (curl|bash, data exfiltration)",
|
|
26
|
+
"Default file system protection (system files modification)",
|
|
27
|
+
"Default package execution control (npm install, npx)",
|
|
28
|
+
"Classifier-based safety judgments for all unlisted tools",
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
// Utility functions
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Read autoMode section from a settings JSON file.
|
|
37
|
+
* fail-safe: returns null on any error (file missing, parse error, no autoMode).
|
|
38
|
+
* @param {string} filePath - Path to settings JSON file
|
|
39
|
+
* @returns {{ soft_deny: string[], soft_allow: string[], environment: string|null }|null}
|
|
40
|
+
*/
|
|
41
|
+
function readSettingsFile(filePath) {
|
|
42
|
+
try {
|
|
43
|
+
if (!fs.existsSync(filePath)) return null;
|
|
44
|
+
const content = JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
45
|
+
const autoMode = content.autoMode;
|
|
46
|
+
if (!autoMode || typeof autoMode !== "object") return null;
|
|
47
|
+
return {
|
|
48
|
+
soft_deny: Array.isArray(autoMode.soft_deny) ? autoMode.soft_deny : [],
|
|
49
|
+
soft_allow: Array.isArray(autoMode.soft_allow) ? autoMode.soft_allow : [],
|
|
50
|
+
environment:
|
|
51
|
+
typeof autoMode.environment === "string" ? autoMode.environment : null,
|
|
52
|
+
};
|
|
53
|
+
} catch {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Classify danger level based on soft_deny and soft_allow counts.
|
|
60
|
+
* @param {number} softDenyCount
|
|
61
|
+
* @param {number} softAllowCount
|
|
62
|
+
* @returns {"safe"|"warn"|"critical"}
|
|
63
|
+
*/
|
|
64
|
+
function classifyDangerLevel(softDenyCount, softAllowCount) {
|
|
65
|
+
if (softDenyCount > 0) return "critical";
|
|
66
|
+
if (softAllowCount > 0) return "warn";
|
|
67
|
+
return "safe";
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// ---------------------------------------------------------------------------
|
|
71
|
+
// Main detection function
|
|
72
|
+
// ---------------------------------------------------------------------------
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Detect Auto Mode configuration and assess danger level.
|
|
76
|
+
* Reads settings.local.json (priority) and settings.json, merges results.
|
|
77
|
+
* fail-safe: never throws, returns safe defaults on any error.
|
|
78
|
+
*
|
|
79
|
+
* @returns {{
|
|
80
|
+
* detected: boolean,
|
|
81
|
+
* danger_level: "safe"|"warn"|"critical",
|
|
82
|
+
* reason: string,
|
|
83
|
+
* soft_deny_count: number,
|
|
84
|
+
* soft_allow_count: number,
|
|
85
|
+
* has_environment: boolean,
|
|
86
|
+
* environment_snippet: string|null,
|
|
87
|
+
* danger_items: string[],
|
|
88
|
+
* source: "settings_local"|"settings"|"both"|"none",
|
|
89
|
+
* detected_at: string
|
|
90
|
+
* }}
|
|
91
|
+
*/
|
|
92
|
+
function detectAutoMode() {
|
|
93
|
+
const detected_at = new Date().toISOString();
|
|
94
|
+
const base = {
|
|
95
|
+
detected: false,
|
|
96
|
+
danger_level: "safe",
|
|
97
|
+
reason: "not_configured",
|
|
98
|
+
soft_deny_count: 0,
|
|
99
|
+
soft_allow_count: 0,
|
|
100
|
+
has_environment: false,
|
|
101
|
+
environment_snippet: null,
|
|
102
|
+
danger_items: [],
|
|
103
|
+
source: "none",
|
|
104
|
+
detected_at,
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
try {
|
|
108
|
+
const localResult = readSettingsFile(SETTINGS_LOCAL);
|
|
109
|
+
const mainResult = readSettingsFile(SETTINGS_MAIN);
|
|
110
|
+
|
|
111
|
+
// No autoMode in either file
|
|
112
|
+
if (!localResult && !mainResult) {
|
|
113
|
+
return base;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Determine source
|
|
117
|
+
const source =
|
|
118
|
+
localResult && mainResult
|
|
119
|
+
? "both"
|
|
120
|
+
: localResult
|
|
121
|
+
? "settings_local"
|
|
122
|
+
: "settings";
|
|
123
|
+
|
|
124
|
+
// Merge soft_deny and soft_allow from both files (deduplicate)
|
|
125
|
+
const softDeny = [
|
|
126
|
+
...new Set([
|
|
127
|
+
...(localResult ? localResult.soft_deny : []),
|
|
128
|
+
...(mainResult ? mainResult.soft_deny : []),
|
|
129
|
+
]),
|
|
130
|
+
];
|
|
131
|
+
const softAllow = [
|
|
132
|
+
...new Set([
|
|
133
|
+
...(localResult ? localResult.soft_allow : []),
|
|
134
|
+
...(mainResult ? mainResult.soft_allow : []),
|
|
135
|
+
]),
|
|
136
|
+
];
|
|
137
|
+
|
|
138
|
+
// Environment: local overrides main
|
|
139
|
+
const environment =
|
|
140
|
+
(localResult && localResult.environment) ||
|
|
141
|
+
(mainResult && mainResult.environment) ||
|
|
142
|
+
null;
|
|
143
|
+
|
|
144
|
+
const dangerLevel = classifyDangerLevel(softDeny.length, softAllow.length);
|
|
145
|
+
|
|
146
|
+
// Determine reason code
|
|
147
|
+
let reason = "configured_safe";
|
|
148
|
+
if (softDeny.length > 0 && softAllow.length > 0) {
|
|
149
|
+
reason = "both_present";
|
|
150
|
+
} else if (softDeny.length > 0) {
|
|
151
|
+
reason = "soft_deny_present";
|
|
152
|
+
} else if (softAllow.length > 0) {
|
|
153
|
+
reason = "soft_allow_only";
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return {
|
|
157
|
+
detected: true,
|
|
158
|
+
danger_level: dangerLevel,
|
|
159
|
+
reason,
|
|
160
|
+
soft_deny_count: softDeny.length,
|
|
161
|
+
soft_allow_count: softAllow.length,
|
|
162
|
+
has_environment: environment !== null,
|
|
163
|
+
environment_snippet: environment ? environment.slice(0, 80) : null,
|
|
164
|
+
danger_items: dangerLevel === "critical" ? [...LOST_PROTECTIONS] : [],
|
|
165
|
+
source,
|
|
166
|
+
detected_at,
|
|
167
|
+
};
|
|
168
|
+
} catch {
|
|
169
|
+
return { ...base, reason: "read_error" };
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// ---------------------------------------------------------------------------
|
|
174
|
+
// Exports
|
|
175
|
+
// ---------------------------------------------------------------------------
|
|
176
|
+
|
|
177
|
+
module.exports = {
|
|
178
|
+
detectAutoMode,
|
|
179
|
+
classifyDangerLevel,
|
|
180
|
+
readSettingsFile,
|
|
181
|
+
LOST_PROTECTIONS,
|
|
182
|
+
SETTINGS_LOCAL,
|
|
183
|
+
SETTINGS_MAIN,
|
|
184
|
+
};
|
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// policy-drift.js — Detect drift between permissions-spec.json deny rules and OpenShell policy YAML
|
|
3
|
+
// Part of Shield Harness: ensures policy files stay in sync with the canonical deny rules
|
|
4
|
+
"use strict";
|
|
5
|
+
|
|
6
|
+
const fs = require("fs");
|
|
7
|
+
const path = require("path");
|
|
8
|
+
|
|
9
|
+
// --- Dependencies (local modules) ---
|
|
10
|
+
|
|
11
|
+
const { loadPermissionsSpec } = require("./permissions-validator");
|
|
12
|
+
const { classifyRulesByDomain } = require("./tier-policy-gen");
|
|
13
|
+
|
|
14
|
+
// --- YAML Parsing (regex-based, no js-yaml dependency) ---
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Extract a YAML list section by name.
|
|
18
|
+
* Expects predictable structure generated by tier-policy-gen.js:
|
|
19
|
+
* section_name:
|
|
20
|
+
* - item1
|
|
21
|
+
* - item2
|
|
22
|
+
*
|
|
23
|
+
* @param {string} content - Full YAML file content
|
|
24
|
+
* @param {string} sectionName - Name of the section (e.g., "deny_read")
|
|
25
|
+
* @returns {string[]} List of items found under the section
|
|
26
|
+
*/
|
|
27
|
+
function extractYamlList(content, sectionName) {
|
|
28
|
+
// Match section header followed by indented list items
|
|
29
|
+
var pattern = new RegExp(
|
|
30
|
+
sectionName + ":\\n((?:[ \\t]+-[ \\t]+.+\\n)*)",
|
|
31
|
+
"m",
|
|
32
|
+
);
|
|
33
|
+
var match = content.match(pattern);
|
|
34
|
+
if (!match) return [];
|
|
35
|
+
return match[1]
|
|
36
|
+
.split("\n")
|
|
37
|
+
.filter(function (l) {
|
|
38
|
+
return l.trim().startsWith("- ");
|
|
39
|
+
})
|
|
40
|
+
.map(function (l) {
|
|
41
|
+
return l.trim().replace(/^-\s+/, "");
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Extract blocked network operations from YAML comment block.
|
|
47
|
+
* Expects format generated by tier-policy-gen.js:
|
|
48
|
+
* # Blocked network operations (from permissions-spec.json deny rules):
|
|
49
|
+
* # - curl *
|
|
50
|
+
* # - wget *
|
|
51
|
+
*
|
|
52
|
+
* @param {string} content - Full YAML file content
|
|
53
|
+
* @returns {string[]} List of blocked network commands
|
|
54
|
+
*/
|
|
55
|
+
function extractBlockedNetworkComments(content) {
|
|
56
|
+
var pattern =
|
|
57
|
+
/# Blocked network operations[^\n]*:\n((?:#[ \t]+-[ \t]+.+\n)*)/m;
|
|
58
|
+
var match = content.match(pattern);
|
|
59
|
+
if (!match) return [];
|
|
60
|
+
return match[1]
|
|
61
|
+
.split("\n")
|
|
62
|
+
.filter(function (l) {
|
|
63
|
+
return l.trim().startsWith("#");
|
|
64
|
+
})
|
|
65
|
+
.map(function (l) {
|
|
66
|
+
return l.trim().replace(/^#\s+-\s+/, "");
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Parse all relevant sections from a policy YAML file.
|
|
72
|
+
* Returns structured data without requiring js-yaml.
|
|
73
|
+
*
|
|
74
|
+
* @param {string} content - Full YAML file content
|
|
75
|
+
* @returns {{
|
|
76
|
+
* denyRead: string[],
|
|
77
|
+
* denyWrite: string[],
|
|
78
|
+
* readWrite: string[],
|
|
79
|
+
* blockedNetwork: string[]
|
|
80
|
+
* }}
|
|
81
|
+
*/
|
|
82
|
+
function parsePolicyYaml(content) {
|
|
83
|
+
return {
|
|
84
|
+
denyRead: extractYamlList(content, "deny_read"),
|
|
85
|
+
denyWrite: extractYamlList(content, "deny_write"),
|
|
86
|
+
readWrite: extractYamlList(content, "read_write"),
|
|
87
|
+
blockedNetwork: extractBlockedNetworkComments(content),
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// --- Drift Detection ---
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Check drift between permissions-spec deny rules and OpenShell policy YAML.
|
|
95
|
+
*
|
|
96
|
+
* Compares the classified deny rules from permissions-spec.json against
|
|
97
|
+
* the content of policy YAML files in the specified directory.
|
|
98
|
+
*
|
|
99
|
+
* Drift rules:
|
|
100
|
+
* 1. Missing filesystem deny (read): spec denyRead paths not in policy deny_read
|
|
101
|
+
* 2. Missing filesystem deny (write): spec denyWrite paths not in policy deny_write
|
|
102
|
+
* 3. Policy allows denied: paths in denyRead/denyWrite that also appear in read_write
|
|
103
|
+
* 4. Missing network deny: network commands not in blocked network comments
|
|
104
|
+
* 5. No policy files: skip (no drift reported)
|
|
105
|
+
* 6. No spec file: skip (no drift reported)
|
|
106
|
+
* 7. Parse errors: fail-safe (no drift, warning logged)
|
|
107
|
+
*
|
|
108
|
+
* @param {{ specPath: string, policyDir: string }} params
|
|
109
|
+
* @returns {{
|
|
110
|
+
* has_drift: boolean,
|
|
111
|
+
* warnings: string[],
|
|
112
|
+
* details: {
|
|
113
|
+
* missing_filesystem_deny: string[],
|
|
114
|
+
* missing_network_deny: string[],
|
|
115
|
+
* policy_allows_denied: string[]
|
|
116
|
+
* },
|
|
117
|
+
* checked_at: string
|
|
118
|
+
* }}
|
|
119
|
+
*/
|
|
120
|
+
function checkPolicyDrift({ specPath, policyDir }) {
|
|
121
|
+
var emptyResult = {
|
|
122
|
+
has_drift: false,
|
|
123
|
+
warnings: [],
|
|
124
|
+
details: {
|
|
125
|
+
missing_filesystem_deny: [],
|
|
126
|
+
missing_network_deny: [],
|
|
127
|
+
policy_allows_denied: [],
|
|
128
|
+
},
|
|
129
|
+
checked_at: new Date().toISOString(),
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
// Rule 6: No spec file — skip silently
|
|
133
|
+
if (!specPath || !fs.existsSync(specPath)) {
|
|
134
|
+
return emptyResult;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Rule 5: No policy directory or no .yaml files — skip silently
|
|
138
|
+
if (!policyDir || !fs.existsSync(policyDir)) {
|
|
139
|
+
return emptyResult;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
var policyFiles;
|
|
143
|
+
try {
|
|
144
|
+
policyFiles = fs.readdirSync(policyDir).filter(function (f) {
|
|
145
|
+
return f.endsWith(".yaml") || f.endsWith(".yml");
|
|
146
|
+
});
|
|
147
|
+
} catch (e) {
|
|
148
|
+
return emptyResult;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (policyFiles.length === 0) {
|
|
152
|
+
return emptyResult;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Load and classify spec deny rules
|
|
156
|
+
var spec;
|
|
157
|
+
try {
|
|
158
|
+
spec = loadPermissionsSpec(specPath);
|
|
159
|
+
} catch (err) {
|
|
160
|
+
return {
|
|
161
|
+
has_drift: false,
|
|
162
|
+
warnings: ["parse_error: spec load failed \u2014 " + err.message],
|
|
163
|
+
details: {
|
|
164
|
+
missing_filesystem_deny: [],
|
|
165
|
+
missing_network_deny: [],
|
|
166
|
+
policy_allows_denied: [],
|
|
167
|
+
},
|
|
168
|
+
checked_at: new Date().toISOString(),
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
var classified;
|
|
173
|
+
try {
|
|
174
|
+
classified = classifyRulesByDomain(spec.deny);
|
|
175
|
+
} catch (err) {
|
|
176
|
+
return {
|
|
177
|
+
has_drift: false,
|
|
178
|
+
warnings: [
|
|
179
|
+
"parse_error: rule classification failed \u2014 " + err.message,
|
|
180
|
+
],
|
|
181
|
+
details: {
|
|
182
|
+
missing_filesystem_deny: [],
|
|
183
|
+
missing_network_deny: [],
|
|
184
|
+
policy_allows_denied: [],
|
|
185
|
+
},
|
|
186
|
+
checked_at: new Date().toISOString(),
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Aggregate policy content from all YAML files
|
|
191
|
+
var aggregated = {
|
|
192
|
+
denyRead: new Set(),
|
|
193
|
+
denyWrite: new Set(),
|
|
194
|
+
readWrite: new Set(),
|
|
195
|
+
blockedNetwork: new Set(),
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
for (var i = 0; i < policyFiles.length; i++) {
|
|
199
|
+
var filePath = path.join(policyDir, policyFiles[i]);
|
|
200
|
+
var content;
|
|
201
|
+
try {
|
|
202
|
+
content = fs.readFileSync(filePath, "utf8");
|
|
203
|
+
} catch (err) {
|
|
204
|
+
// Rule 7: Parse error — fail-safe
|
|
205
|
+
return {
|
|
206
|
+
has_drift: false,
|
|
207
|
+
warnings: [
|
|
208
|
+
"parse_error: cannot read " + filePath + " \u2014 " + err.message,
|
|
209
|
+
],
|
|
210
|
+
details: {
|
|
211
|
+
missing_filesystem_deny: [],
|
|
212
|
+
missing_network_deny: [],
|
|
213
|
+
policy_allows_denied: [],
|
|
214
|
+
},
|
|
215
|
+
checked_at: new Date().toISOString(),
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
var parsed;
|
|
220
|
+
try {
|
|
221
|
+
parsed = parsePolicyYaml(content);
|
|
222
|
+
} catch (err) {
|
|
223
|
+
return {
|
|
224
|
+
has_drift: false,
|
|
225
|
+
warnings: [
|
|
226
|
+
"parse_error: cannot parse " + filePath + " \u2014 " + err.message,
|
|
227
|
+
],
|
|
228
|
+
details: {
|
|
229
|
+
missing_filesystem_deny: [],
|
|
230
|
+
missing_network_deny: [],
|
|
231
|
+
policy_allows_denied: [],
|
|
232
|
+
},
|
|
233
|
+
checked_at: new Date().toISOString(),
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
parsed.denyRead.forEach(function (p) {
|
|
238
|
+
aggregated.denyRead.add(p);
|
|
239
|
+
});
|
|
240
|
+
parsed.denyWrite.forEach(function (p) {
|
|
241
|
+
aggregated.denyWrite.add(p);
|
|
242
|
+
});
|
|
243
|
+
parsed.readWrite.forEach(function (p) {
|
|
244
|
+
aggregated.readWrite.add(p);
|
|
245
|
+
});
|
|
246
|
+
parsed.blockedNetwork.forEach(function (cmd) {
|
|
247
|
+
aggregated.blockedNetwork.add(cmd);
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// --- Compare classified rules against aggregated policy ---
|
|
252
|
+
|
|
253
|
+
var warnings = [];
|
|
254
|
+
var missingFilesystemDeny = [];
|
|
255
|
+
var missingNetworkDeny = [];
|
|
256
|
+
var policyAllowsDenied = [];
|
|
257
|
+
|
|
258
|
+
// Rule 1: Missing filesystem deny (read)
|
|
259
|
+
for (var ri = 0; ri < classified.denyRead.length; ri++) {
|
|
260
|
+
var readPath = classified.denyRead[ri];
|
|
261
|
+
if (!aggregated.denyRead.has(readPath)) {
|
|
262
|
+
warnings.push("deny_read missing in policy: " + readPath);
|
|
263
|
+
missingFilesystemDeny.push(readPath);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Rule 2: Missing filesystem deny (write)
|
|
268
|
+
for (var wi = 0; wi < classified.denyWrite.length; wi++) {
|
|
269
|
+
var writePath = classified.denyWrite[wi];
|
|
270
|
+
if (!aggregated.denyWrite.has(writePath)) {
|
|
271
|
+
warnings.push("deny_write missing in policy: " + writePath);
|
|
272
|
+
missingFilesystemDeny.push(writePath);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Rule 3: Policy allows denied — conflict detection
|
|
277
|
+
var allDeniedPaths = new Set(
|
|
278
|
+
classified.denyRead.concat(classified.denyWrite),
|
|
279
|
+
);
|
|
280
|
+
aggregated.readWrite.forEach(function (rwPath) {
|
|
281
|
+
if (allDeniedPaths.has(rwPath)) {
|
|
282
|
+
warnings.push("conflict: read_write allows denied path: " + rwPath);
|
|
283
|
+
policyAllowsDenied.push(rwPath);
|
|
284
|
+
}
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
// Rule 4: Missing network deny
|
|
288
|
+
for (var ni = 0; ni < classified.network.length; ni++) {
|
|
289
|
+
var netCmd = classified.network[ni];
|
|
290
|
+
if (!aggregated.blockedNetwork.has(netCmd)) {
|
|
291
|
+
warnings.push(
|
|
292
|
+
"blocked network command missing in policy comments: " + netCmd,
|
|
293
|
+
);
|
|
294
|
+
missingNetworkDeny.push(netCmd);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
var hasDrift =
|
|
299
|
+
missingFilesystemDeny.length > 0 ||
|
|
300
|
+
missingNetworkDeny.length > 0 ||
|
|
301
|
+
policyAllowsDenied.length > 0;
|
|
302
|
+
|
|
303
|
+
return {
|
|
304
|
+
has_drift: hasDrift,
|
|
305
|
+
warnings: warnings,
|
|
306
|
+
details: {
|
|
307
|
+
missing_filesystem_deny: missingFilesystemDeny,
|
|
308
|
+
missing_network_deny: missingNetworkDeny,
|
|
309
|
+
policy_allows_denied: policyAllowsDenied,
|
|
310
|
+
},
|
|
311
|
+
checked_at: new Date().toISOString(),
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
module.exports = {
|
|
316
|
+
// Main entry point
|
|
317
|
+
checkPolicyDrift: checkPolicyDrift,
|
|
318
|
+
// Parsing helpers (exported for unit testing)
|
|
319
|
+
extractYamlList: extractYamlList,
|
|
320
|
+
extractBlockedNetworkComments: extractBlockedNetworkComments,
|
|
321
|
+
parsePolicyYaml: parsePolicyYaml,
|
|
322
|
+
};
|