defense-mcp-server 0.6.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/CHANGELOG.md +471 -0
- package/LICENSE +21 -0
- package/README.md +242 -0
- package/build/core/auto-installer.d.ts +102 -0
- package/build/core/auto-installer.d.ts.map +1 -0
- package/build/core/auto-installer.js +833 -0
- package/build/core/backup-manager.d.ts +63 -0
- package/build/core/backup-manager.d.ts.map +1 -0
- package/build/core/backup-manager.js +189 -0
- package/build/core/changelog.d.ts +75 -0
- package/build/core/changelog.d.ts.map +1 -0
- package/build/core/changelog.js +123 -0
- package/build/core/command-allowlist.d.ts +129 -0
- package/build/core/command-allowlist.d.ts.map +1 -0
- package/build/core/command-allowlist.js +849 -0
- package/build/core/config.d.ts +79 -0
- package/build/core/config.d.ts.map +1 -0
- package/build/core/config.js +193 -0
- package/build/core/dependency-validator.d.ts +106 -0
- package/build/core/dependency-validator.d.ts.map +1 -0
- package/build/core/dependency-validator.js +405 -0
- package/build/core/distro-adapter.d.ts +177 -0
- package/build/core/distro-adapter.d.ts.map +1 -0
- package/build/core/distro-adapter.js +481 -0
- package/build/core/distro.d.ts +68 -0
- package/build/core/distro.d.ts.map +1 -0
- package/build/core/distro.js +457 -0
- package/build/core/encrypted-state.d.ts +76 -0
- package/build/core/encrypted-state.d.ts.map +1 -0
- package/build/core/encrypted-state.js +209 -0
- package/build/core/executor.d.ts +56 -0
- package/build/core/executor.d.ts.map +1 -0
- package/build/core/executor.js +350 -0
- package/build/core/installer.d.ts +92 -0
- package/build/core/installer.d.ts.map +1 -0
- package/build/core/installer.js +1072 -0
- package/build/core/logger.d.ts +102 -0
- package/build/core/logger.d.ts.map +1 -0
- package/build/core/logger.js +132 -0
- package/build/core/parsers.d.ts +151 -0
- package/build/core/parsers.d.ts.map +1 -0
- package/build/core/parsers.js +479 -0
- package/build/core/policy-engine.d.ts +170 -0
- package/build/core/policy-engine.d.ts.map +1 -0
- package/build/core/policy-engine.js +656 -0
- package/build/core/preflight.d.ts +157 -0
- package/build/core/preflight.d.ts.map +1 -0
- package/build/core/preflight.js +638 -0
- package/build/core/privilege-manager.d.ts +108 -0
- package/build/core/privilege-manager.d.ts.map +1 -0
- package/build/core/privilege-manager.js +363 -0
- package/build/core/rate-limiter.d.ts +67 -0
- package/build/core/rate-limiter.d.ts.map +1 -0
- package/build/core/rate-limiter.js +129 -0
- package/build/core/rollback.d.ts +73 -0
- package/build/core/rollback.d.ts.map +1 -0
- package/build/core/rollback.js +278 -0
- package/build/core/safeguards.d.ts +58 -0
- package/build/core/safeguards.d.ts.map +1 -0
- package/build/core/safeguards.js +448 -0
- package/build/core/sanitizer.d.ts +118 -0
- package/build/core/sanitizer.d.ts.map +1 -0
- package/build/core/sanitizer.js +459 -0
- package/build/core/secure-fs.d.ts +67 -0
- package/build/core/secure-fs.d.ts.map +1 -0
- package/build/core/secure-fs.js +143 -0
- package/build/core/spawn-safe.d.ts +55 -0
- package/build/core/spawn-safe.d.ts.map +1 -0
- package/build/core/spawn-safe.js +146 -0
- package/build/core/sudo-guard.d.ts +145 -0
- package/build/core/sudo-guard.d.ts.map +1 -0
- package/build/core/sudo-guard.js +349 -0
- package/build/core/sudo-session.d.ts +100 -0
- package/build/core/sudo-session.d.ts.map +1 -0
- package/build/core/sudo-session.js +319 -0
- package/build/core/tool-dependencies.d.ts +61 -0
- package/build/core/tool-dependencies.d.ts.map +1 -0
- package/build/core/tool-dependencies.js +571 -0
- package/build/core/tool-registry.d.ts +111 -0
- package/build/core/tool-registry.d.ts.map +1 -0
- package/build/core/tool-registry.js +656 -0
- package/build/core/tool-wrapper.d.ts +73 -0
- package/build/core/tool-wrapper.d.ts.map +1 -0
- package/build/core/tool-wrapper.js +296 -0
- package/build/index.d.ts +3 -0
- package/build/index.d.ts.map +1 -0
- package/build/index.js +247 -0
- package/build/tools/access-control.d.ts +9 -0
- package/build/tools/access-control.d.ts.map +1 -0
- package/build/tools/access-control.js +1818 -0
- package/build/tools/api-security.d.ts +12 -0
- package/build/tools/api-security.d.ts.map +1 -0
- package/build/tools/api-security.js +901 -0
- package/build/tools/app-hardening.d.ts +11 -0
- package/build/tools/app-hardening.d.ts.map +1 -0
- package/build/tools/app-hardening.js +768 -0
- package/build/tools/backup.d.ts +8 -0
- package/build/tools/backup.d.ts.map +1 -0
- package/build/tools/backup.js +381 -0
- package/build/tools/cloud-security.d.ts +17 -0
- package/build/tools/cloud-security.d.ts.map +1 -0
- package/build/tools/cloud-security.js +739 -0
- package/build/tools/compliance.d.ts +10 -0
- package/build/tools/compliance.d.ts.map +1 -0
- package/build/tools/compliance.js +1225 -0
- package/build/tools/container-security.d.ts +14 -0
- package/build/tools/container-security.d.ts.map +1 -0
- package/build/tools/container-security.js +788 -0
- package/build/tools/deception.d.ts +13 -0
- package/build/tools/deception.d.ts.map +1 -0
- package/build/tools/deception.js +763 -0
- package/build/tools/dns-security.d.ts +93 -0
- package/build/tools/dns-security.d.ts.map +1 -0
- package/build/tools/dns-security.js +745 -0
- package/build/tools/drift-detection.d.ts +8 -0
- package/build/tools/drift-detection.d.ts.map +1 -0
- package/build/tools/drift-detection.js +326 -0
- package/build/tools/ebpf-security.d.ts +15 -0
- package/build/tools/ebpf-security.d.ts.map +1 -0
- package/build/tools/ebpf-security.js +294 -0
- package/build/tools/encryption.d.ts +9 -0
- package/build/tools/encryption.d.ts.map +1 -0
- package/build/tools/encryption.js +1667 -0
- package/build/tools/firewall.d.ts +9 -0
- package/build/tools/firewall.d.ts.map +1 -0
- package/build/tools/firewall.js +1398 -0
- package/build/tools/hardening.d.ts +10 -0
- package/build/tools/hardening.d.ts.map +1 -0
- package/build/tools/hardening.js +2654 -0
- package/build/tools/ids.d.ts +9 -0
- package/build/tools/ids.d.ts.map +1 -0
- package/build/tools/ids.js +624 -0
- package/build/tools/incident-response.d.ts +10 -0
- package/build/tools/incident-response.d.ts.map +1 -0
- package/build/tools/incident-response.js +1180 -0
- package/build/tools/logging.d.ts +12 -0
- package/build/tools/logging.d.ts.map +1 -0
- package/build/tools/logging.js +454 -0
- package/build/tools/malware.d.ts +10 -0
- package/build/tools/malware.d.ts.map +1 -0
- package/build/tools/malware.js +532 -0
- package/build/tools/meta.d.ts +11 -0
- package/build/tools/meta.d.ts.map +1 -0
- package/build/tools/meta.js +2278 -0
- package/build/tools/network-defense.d.ts +12 -0
- package/build/tools/network-defense.d.ts.map +1 -0
- package/build/tools/network-defense.js +760 -0
- package/build/tools/patch-management.d.ts +3 -0
- package/build/tools/patch-management.d.ts.map +1 -0
- package/build/tools/patch-management.js +708 -0
- package/build/tools/process-security.d.ts +12 -0
- package/build/tools/process-security.d.ts.map +1 -0
- package/build/tools/process-security.js +784 -0
- package/build/tools/reporting.d.ts +11 -0
- package/build/tools/reporting.d.ts.map +1 -0
- package/build/tools/reporting.js +559 -0
- package/build/tools/secrets.d.ts +9 -0
- package/build/tools/secrets.d.ts.map +1 -0
- package/build/tools/secrets.js +596 -0
- package/build/tools/siem-integration.d.ts +18 -0
- package/build/tools/siem-integration.d.ts.map +1 -0
- package/build/tools/siem-integration.js +754 -0
- package/build/tools/sudo-management.d.ts +18 -0
- package/build/tools/sudo-management.d.ts.map +1 -0
- package/build/tools/sudo-management.js +737 -0
- package/build/tools/supply-chain-security.d.ts +8 -0
- package/build/tools/supply-chain-security.d.ts.map +1 -0
- package/build/tools/supply-chain-security.js +256 -0
- package/build/tools/threat-intel.d.ts +22 -0
- package/build/tools/threat-intel.d.ts.map +1 -0
- package/build/tools/threat-intel.js +749 -0
- package/build/tools/vulnerability-management.d.ts +11 -0
- package/build/tools/vulnerability-management.d.ts.map +1 -0
- package/build/tools/vulnerability-management.js +667 -0
- package/build/tools/waf.d.ts +12 -0
- package/build/tools/waf.d.ts.map +1 -0
- package/build/tools/waf.js +843 -0
- package/build/tools/wireless-security.d.ts +19 -0
- package/build/tools/wireless-security.d.ts.map +1 -0
- package/build/tools/wireless-security.js +826 -0
- package/build/tools/zero-trust-network.d.ts +8 -0
- package/build/tools/zero-trust-network.d.ts.map +1 -0
- package/build/tools/zero-trust-network.js +367 -0
- package/docs/SAFEGUARDS.md +518 -0
- package/docs/TOOLS-REFERENCE.md +665 -0
- package/package.json +87 -0
|
@@ -0,0 +1,656 @@
|
|
|
1
|
+
import { readFileSync, readdirSync } from "node:fs";
|
|
2
|
+
import { dirname, basename } from "node:path";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { executeCommand } from "./executor.js";
|
|
5
|
+
import { getConfig } from "./config.js";
|
|
6
|
+
import { isAllowlisted } from "./command-allowlist.js";
|
|
7
|
+
import { secureWriteFileSync, secureMkdirSync } from "./secure-fs.js";
|
|
8
|
+
// ── Zod Schemas for Policy Validation ────────────────────────────────────────
|
|
9
|
+
const PolicySeveritySchema = z.enum([
|
|
10
|
+
"critical",
|
|
11
|
+
"high",
|
|
12
|
+
"medium",
|
|
13
|
+
"low",
|
|
14
|
+
"info",
|
|
15
|
+
]);
|
|
16
|
+
/**
|
|
17
|
+
* Zod schema for a single policy rule.
|
|
18
|
+
* Fields that may be absent in legacy policy files have defaults.
|
|
19
|
+
*/
|
|
20
|
+
const PolicyRuleSchema = z.object({
|
|
21
|
+
id: z.string().min(1).max(128),
|
|
22
|
+
title: z.string().max(256).default(""),
|
|
23
|
+
description: z.string().max(1024).default(""),
|
|
24
|
+
severity: PolicySeveritySchema.default("medium"),
|
|
25
|
+
category: z.string().max(128).default("general"),
|
|
26
|
+
check: z.array(z.string().max(1024)).min(1).max(50),
|
|
27
|
+
// SECURITY (CORE-009): Limit expectedOutput regex to 200 chars to match safeRegexTest limit
|
|
28
|
+
expectedOutput: z.string().max(200).optional(),
|
|
29
|
+
remediation: z.array(z.string().max(1024)).min(1).max(50).optional(),
|
|
30
|
+
references: z.array(z.string().max(256)).max(20).optional(),
|
|
31
|
+
});
|
|
32
|
+
/**
|
|
33
|
+
* Zod schema for a complete policy set.
|
|
34
|
+
*/
|
|
35
|
+
const PolicySetSchema = z.object({
|
|
36
|
+
name: z.string().min(1).max(256),
|
|
37
|
+
version: z.string().max(64).default("1.0.0"),
|
|
38
|
+
description: z.string().max(2048).default(""),
|
|
39
|
+
rules: z.array(PolicyRuleSchema).min(1).max(200),
|
|
40
|
+
});
|
|
41
|
+
// ── Security Validation ──────────────────────────────────────────────────────
|
|
42
|
+
/**
|
|
43
|
+
* Shell interpreters that are explicitly blocked in policy rules.
|
|
44
|
+
* Even if somehow added to the command allowlist, these must never be
|
|
45
|
+
* invoked by a policy rule — they would enable arbitrary command execution.
|
|
46
|
+
*/
|
|
47
|
+
const BLOCKED_INTERPRETERS = new Set([
|
|
48
|
+
"sh",
|
|
49
|
+
"bash",
|
|
50
|
+
"zsh",
|
|
51
|
+
"fish",
|
|
52
|
+
"csh",
|
|
53
|
+
"dash",
|
|
54
|
+
"ksh",
|
|
55
|
+
"tcsh",
|
|
56
|
+
"/bin/sh",
|
|
57
|
+
"/bin/bash",
|
|
58
|
+
"/bin/zsh",
|
|
59
|
+
"/bin/dash",
|
|
60
|
+
"/bin/csh",
|
|
61
|
+
"/bin/ksh",
|
|
62
|
+
"/bin/tcsh",
|
|
63
|
+
"/bin/fish",
|
|
64
|
+
"/usr/bin/sh",
|
|
65
|
+
"/usr/bin/bash",
|
|
66
|
+
"/usr/bin/zsh",
|
|
67
|
+
"/usr/bin/dash",
|
|
68
|
+
"/usr/bin/csh",
|
|
69
|
+
"/usr/bin/ksh",
|
|
70
|
+
"/usr/bin/tcsh",
|
|
71
|
+
"/usr/bin/fish",
|
|
72
|
+
]);
|
|
73
|
+
/** Control characters regex — matches dangerous non-printable characters */
|
|
74
|
+
const CONTROL_CHAR_RE = /[\x00-\x08\x0e-\x1f\x7f]/;
|
|
75
|
+
/**
|
|
76
|
+
* Validates a policy rule's check (or remediation) command array.
|
|
77
|
+
*
|
|
78
|
+
* Security controls:
|
|
79
|
+
* 1. Command (check[0]) must be in the security allowlist
|
|
80
|
+
* 2. Shell interpreters are explicitly blocked (even if allowlisted)
|
|
81
|
+
* 3. Arguments are checked for null bytes and control characters
|
|
82
|
+
*
|
|
83
|
+
* Note: Shell metacharacters (|, &, $, etc.) in arguments are NOT blocked
|
|
84
|
+
* because policy rules use execFile (no shell), making these characters
|
|
85
|
+
* harmless literal values. Policy rules legitimately need regex
|
|
86
|
+
* metacharacters as arguments to grep/awk/sed.
|
|
87
|
+
*
|
|
88
|
+
* @param check The command array [command, ...args]
|
|
89
|
+
* @param label Human-readable label for error messages (e.g., "check", "remediation")
|
|
90
|
+
* @throws {Error} If validation fails
|
|
91
|
+
*/
|
|
92
|
+
export function validateRuleCheck(check, label = "check") {
|
|
93
|
+
if (!Array.isArray(check) || check.length === 0) {
|
|
94
|
+
throw new Error(`Policy rule ${label} must be a non-empty array`);
|
|
95
|
+
}
|
|
96
|
+
const command = check[0];
|
|
97
|
+
// Block shell interpreters explicitly — this is defense-in-depth
|
|
98
|
+
// even if they were somehow added to the command allowlist
|
|
99
|
+
if (BLOCKED_INTERPRETERS.has(command)) {
|
|
100
|
+
throw new Error(`Shell interpreter '${command}' is not allowed in policy rules`);
|
|
101
|
+
}
|
|
102
|
+
// Validate command against the security allowlist
|
|
103
|
+
if (!isAllowlisted(command)) {
|
|
104
|
+
throw new Error(`Command '${command}' is not in the security allowlist`);
|
|
105
|
+
}
|
|
106
|
+
// Validate arguments for null bytes and control characters
|
|
107
|
+
const args = check.slice(1);
|
|
108
|
+
for (let i = 0; i < args.length; i++) {
|
|
109
|
+
const arg = args[i];
|
|
110
|
+
if (typeof arg !== "string") {
|
|
111
|
+
throw new Error(`${label} argument at index ${i + 1} is not a string`);
|
|
112
|
+
}
|
|
113
|
+
if (arg.includes("\0")) {
|
|
114
|
+
throw new Error(`${label} argument at index ${i + 1} contains null bytes`);
|
|
115
|
+
}
|
|
116
|
+
if (CONTROL_CHAR_RE.test(arg)) {
|
|
117
|
+
throw new Error(`${label} argument at index ${i + 1} contains control characters`);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* SECURITY (CORE-009): ReDoS (Regular Expression Denial of Service) protection.
|
|
123
|
+
*
|
|
124
|
+
* Safely tests a regex pattern against input with multiple layers of defense
|
|
125
|
+
* against catastrophic backtracking:
|
|
126
|
+
*
|
|
127
|
+
* 1. **Length limit**: Patterns longer than 200 characters are rejected to reduce
|
|
128
|
+
* the attack surface for complex regex injection.
|
|
129
|
+
* 2. **Nested quantifier detection**: Patterns like `(a+)+`, `(a*)*`, `(a+)*`
|
|
130
|
+
* are rejected because they cause exponential backtracking on non-matching
|
|
131
|
+
* input. The check uses two heuristics:
|
|
132
|
+
* - Repeated quantifiers: `a++`, `a**`, `{n,m}{` (possessive-like syntax
|
|
133
|
+
* that JavaScript doesn't support, indicating malformed patterns)
|
|
134
|
+
* - Group-level nesting: `([...]+)+` or `([...]*)*` where a quantified
|
|
135
|
+
* group is itself quantified
|
|
136
|
+
* 3. **try-catch**: Invalid regex syntax is caught and reported clearly.
|
|
137
|
+
*
|
|
138
|
+
* These checks are applied to user-supplied `expectedOutput` regex patterns
|
|
139
|
+
* in policy rules before they are compiled or executed.
|
|
140
|
+
*
|
|
141
|
+
* @param pattern The regex pattern string
|
|
142
|
+
* @param input The string to test against
|
|
143
|
+
* @returns Whether the pattern matches the input
|
|
144
|
+
* @throws {Error} If the pattern is dangerous, invalid, or too long
|
|
145
|
+
*/
|
|
146
|
+
export function safeRegexTest(pattern, input) {
|
|
147
|
+
// 1. Reject excessively long patterns (reduced from 1024 to 200 for CORE-009)
|
|
148
|
+
if (pattern.length > 200) {
|
|
149
|
+
throw new Error("Regex pattern too long (max 200 characters)");
|
|
150
|
+
}
|
|
151
|
+
// 2a. Reject obviously dangerous patterns that cause catastrophic backtracking:
|
|
152
|
+
// - Repeated quantifiers: a++, a**, {n,m}{
|
|
153
|
+
if (/(\+\+|\*\*|\{\d+,\d*\}\{)/.test(pattern)) {
|
|
154
|
+
throw new Error("Regex pattern too complex (potential ReDoS)");
|
|
155
|
+
}
|
|
156
|
+
// 2b. Detect nested quantifiers like (a+)+, (a+)*, ([a-z]+)+
|
|
157
|
+
if (/\([^)]*[+*][^)]*\)[+*{]/.test(pattern)) {
|
|
158
|
+
throw new Error("Regex pattern contains nested quantifiers (potential ReDoS)");
|
|
159
|
+
}
|
|
160
|
+
// 3. Compile and execute with error handling
|
|
161
|
+
try {
|
|
162
|
+
const re = new RegExp(pattern, "m");
|
|
163
|
+
return re.test(input);
|
|
164
|
+
}
|
|
165
|
+
catch {
|
|
166
|
+
throw new Error(`Invalid regex pattern: ${pattern}`);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
// ── Core Functions ───────────────────────────────────────────────────────────
|
|
170
|
+
/**
|
|
171
|
+
* Evaluates a single policy rule by executing its check command
|
|
172
|
+
* and comparing the output against the expected pattern.
|
|
173
|
+
*
|
|
174
|
+
* Before execution, the check command is validated against the
|
|
175
|
+
* security allowlist and shell interpreters are blocked.
|
|
176
|
+
*
|
|
177
|
+
* @param rule The policy rule to evaluate
|
|
178
|
+
* @returns The evaluation result
|
|
179
|
+
*/
|
|
180
|
+
export async function evaluateRule(rule) {
|
|
181
|
+
try {
|
|
182
|
+
if (!rule.check || rule.check.length === 0) {
|
|
183
|
+
return {
|
|
184
|
+
rule,
|
|
185
|
+
passed: false,
|
|
186
|
+
actual: "",
|
|
187
|
+
message: "Rule has no check command defined",
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
// SECURITY: Validate command against allowlist and block shell interpreters
|
|
191
|
+
validateRuleCheck(rule.check, "check");
|
|
192
|
+
const [command, ...args] = rule.check;
|
|
193
|
+
const result = await executeCommand({
|
|
194
|
+
command,
|
|
195
|
+
args,
|
|
196
|
+
timeout: 30_000,
|
|
197
|
+
});
|
|
198
|
+
const actual = result.stdout.trim();
|
|
199
|
+
// If no expected output defined, passing means exit code 0
|
|
200
|
+
if (!rule.expectedOutput) {
|
|
201
|
+
const passed = result.exitCode === 0;
|
|
202
|
+
return {
|
|
203
|
+
rule,
|
|
204
|
+
passed,
|
|
205
|
+
actual,
|
|
206
|
+
message: passed
|
|
207
|
+
? `Check passed (exit code 0)`
|
|
208
|
+
: `Check failed (exit code ${result.exitCode}): ${result.stderr.trim()}`,
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
// Use safe regex test with ReDoS protection
|
|
212
|
+
let passed = false;
|
|
213
|
+
try {
|
|
214
|
+
passed = safeRegexTest(rule.expectedOutput, actual);
|
|
215
|
+
}
|
|
216
|
+
catch (regexErr) {
|
|
217
|
+
// If regex is invalid or dangerous, try exact match as fallback
|
|
218
|
+
const regexMsg = regexErr instanceof Error ? regexErr.message : String(regexErr);
|
|
219
|
+
if (regexMsg.includes("ReDoS") || regexMsg.includes("too complex")) {
|
|
220
|
+
// Don't fallback for ReDoS — that's a security issue
|
|
221
|
+
return {
|
|
222
|
+
rule,
|
|
223
|
+
passed: false,
|
|
224
|
+
actual,
|
|
225
|
+
message: `Error: ${regexMsg}`,
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
// For simple invalid regex, fall back to substring match
|
|
229
|
+
passed = actual.includes(rule.expectedOutput);
|
|
230
|
+
}
|
|
231
|
+
return {
|
|
232
|
+
rule,
|
|
233
|
+
passed,
|
|
234
|
+
actual,
|
|
235
|
+
message: passed
|
|
236
|
+
? `Check passed: output matches expected pattern`
|
|
237
|
+
: `Check failed: expected pattern "${rule.expectedOutput}" not found in output`,
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
catch (err) {
|
|
241
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
242
|
+
return {
|
|
243
|
+
rule,
|
|
244
|
+
passed: false,
|
|
245
|
+
actual: "",
|
|
246
|
+
message: `Error evaluating rule: ${message}`,
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* Evaluates all rules in a policy set and returns a summary.
|
|
252
|
+
*
|
|
253
|
+
* @param policySet The policy set to evaluate
|
|
254
|
+
* @returns Evaluation summary with individual results
|
|
255
|
+
*/
|
|
256
|
+
export async function evaluatePolicy(policySet) {
|
|
257
|
+
const results = [];
|
|
258
|
+
let passed = 0;
|
|
259
|
+
let failed = 0;
|
|
260
|
+
let errors = 0;
|
|
261
|
+
console.error(`[policy-engine] Evaluating policy: ${policySet.name} (${policySet.rules.length} rules)`);
|
|
262
|
+
for (const rule of policySet.rules) {
|
|
263
|
+
const result = await evaluateRule(rule);
|
|
264
|
+
results.push(result);
|
|
265
|
+
if (result.passed) {
|
|
266
|
+
passed++;
|
|
267
|
+
}
|
|
268
|
+
else if (result.message.startsWith("Error")) {
|
|
269
|
+
errors++;
|
|
270
|
+
}
|
|
271
|
+
else {
|
|
272
|
+
failed++;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
const totalRules = policySet.rules.length;
|
|
276
|
+
const compliancePercent = totalRules > 0 ? Math.round((passed / totalRules) * 100) : 0;
|
|
277
|
+
console.error(`[policy-engine] Results: ${passed}/${totalRules} passed (${compliancePercent}% compliance)`);
|
|
278
|
+
return {
|
|
279
|
+
policyName: policySet.name,
|
|
280
|
+
totalRules,
|
|
281
|
+
passed,
|
|
282
|
+
failed,
|
|
283
|
+
errors,
|
|
284
|
+
compliancePercent,
|
|
285
|
+
results,
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* Loads a policy set from a JSON file with strict schema validation.
|
|
290
|
+
*
|
|
291
|
+
* Validates:
|
|
292
|
+
* 1. JSON structure via Zod schema (field types, lengths, required fields)
|
|
293
|
+
* 2. All check commands against the security allowlist
|
|
294
|
+
* 3. All remediation commands against the security allowlist
|
|
295
|
+
*
|
|
296
|
+
* @param path Absolute or relative path to the policy JSON file
|
|
297
|
+
* @returns The loaded and validated policy set
|
|
298
|
+
* @throws If the file cannot be read, parsed, or fails validation
|
|
299
|
+
*/
|
|
300
|
+
export function loadPolicy(path) {
|
|
301
|
+
const content = readFileSync(path, "utf-8");
|
|
302
|
+
const parsed = JSON.parse(content);
|
|
303
|
+
// Validate structure with Zod schema
|
|
304
|
+
const result = PolicySetSchema.safeParse(parsed);
|
|
305
|
+
if (!result.success) {
|
|
306
|
+
const issues = result.error.issues
|
|
307
|
+
.map((i) => `${i.path.join(".")}: ${i.message}`)
|
|
308
|
+
.join("; ");
|
|
309
|
+
throw new Error(`Invalid policy file ${path}: ${issues}`);
|
|
310
|
+
}
|
|
311
|
+
const validated = result.data;
|
|
312
|
+
// Validate all rule commands against the security allowlist
|
|
313
|
+
for (const rule of validated.rules) {
|
|
314
|
+
validateRuleCheck(rule.check, `rule ${rule.id} check`);
|
|
315
|
+
if (rule.remediation) {
|
|
316
|
+
validateRuleCheck(rule.remediation, `rule ${rule.id} remediation`);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
return validated;
|
|
320
|
+
}
|
|
321
|
+
/**
|
|
322
|
+
* Saves a policy set to a JSON file with secure permissions.
|
|
323
|
+
* Creates parent directories with owner-only permissions (0o700).
|
|
324
|
+
* Files are written with owner-only permissions (0o600).
|
|
325
|
+
*
|
|
326
|
+
* @param path Path to save the policy file
|
|
327
|
+
* @param policy The policy set to save
|
|
328
|
+
*/
|
|
329
|
+
export function savePolicy(path, policy) {
|
|
330
|
+
// SECURITY (CORE-013): Explicitly create parent directory via secure-fs
|
|
331
|
+
// (0o700 permissions) instead of relying on bare mkdirSync.
|
|
332
|
+
const parentDir = dirname(path);
|
|
333
|
+
secureMkdirSync(parentDir);
|
|
334
|
+
// Use secureWriteFileSync which writes files with 0o600
|
|
335
|
+
secureWriteFileSync(path, JSON.stringify(policy, null, 2), "utf-8");
|
|
336
|
+
console.error(`[policy-engine] Saved policy to ${path}`);
|
|
337
|
+
}
|
|
338
|
+
/**
|
|
339
|
+
* Returns a list of built-in policy file names from the policy directory.
|
|
340
|
+
* Returns empty array if the directory doesn't exist or is empty.
|
|
341
|
+
*/
|
|
342
|
+
export function getBuiltinPolicies() {
|
|
343
|
+
try {
|
|
344
|
+
const config = getConfig();
|
|
345
|
+
const policyDir = config.policyDir;
|
|
346
|
+
const files = readdirSync(policyDir);
|
|
347
|
+
return files
|
|
348
|
+
.filter((f) => f.endsWith(".json"))
|
|
349
|
+
.map((f) => basename(f, ".json"));
|
|
350
|
+
}
|
|
351
|
+
catch {
|
|
352
|
+
return [];
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
/**
|
|
356
|
+
* Built-in policy rule templates for common hardening checks.
|
|
357
|
+
* These can be used as a starting point for custom policies.
|
|
358
|
+
*
|
|
359
|
+
* SECURITY: All check commands use direct binary invocation (no shell).
|
|
360
|
+
* Shell interpreters (sh, bash, etc.) are never used in check or remediation arrays.
|
|
361
|
+
*/
|
|
362
|
+
export const BUILTIN_RULE_TEMPLATES = [
|
|
363
|
+
{
|
|
364
|
+
id: "KERN-001",
|
|
365
|
+
title: "IP forwarding disabled",
|
|
366
|
+
description: "Ensure IP forwarding is disabled unless the system is a router",
|
|
367
|
+
severity: "high",
|
|
368
|
+
category: "network",
|
|
369
|
+
check: ["sysctl", "-n", "net.ipv4.ip_forward"],
|
|
370
|
+
expectedOutput: "^0$",
|
|
371
|
+
remediation: ["sysctl", "-w", "net.ipv4.ip_forward=0"],
|
|
372
|
+
references: ["CIS-3.1.1", "NIST-SC-7"],
|
|
373
|
+
},
|
|
374
|
+
{
|
|
375
|
+
id: "KERN-002",
|
|
376
|
+
title: "ICMP redirects disabled",
|
|
377
|
+
description: "Ensure ICMP redirects are not accepted",
|
|
378
|
+
severity: "high",
|
|
379
|
+
category: "network",
|
|
380
|
+
check: ["sysctl", "-n", "net.ipv4.conf.all.accept_redirects"],
|
|
381
|
+
expectedOutput: "^0$",
|
|
382
|
+
remediation: ["sysctl", "-w", "net.ipv4.conf.all.accept_redirects=0"],
|
|
383
|
+
references: ["CIS-3.2.2", "NIST-SC-7"],
|
|
384
|
+
},
|
|
385
|
+
{
|
|
386
|
+
id: "KERN-003",
|
|
387
|
+
title: "Source routing disabled",
|
|
388
|
+
description: "Ensure source routed packets are not accepted",
|
|
389
|
+
severity: "high",
|
|
390
|
+
category: "network",
|
|
391
|
+
check: ["sysctl", "-n", "net.ipv4.conf.all.accept_source_route"],
|
|
392
|
+
expectedOutput: "^0$",
|
|
393
|
+
remediation: ["sysctl", "-w", "net.ipv4.conf.all.accept_source_route=0"],
|
|
394
|
+
references: ["CIS-3.2.1", "NIST-SC-7"],
|
|
395
|
+
},
|
|
396
|
+
{
|
|
397
|
+
id: "KERN-004",
|
|
398
|
+
title: "SYN cookies enabled",
|
|
399
|
+
description: "Ensure TCP SYN cookies are enabled to prevent SYN flood attacks",
|
|
400
|
+
severity: "high",
|
|
401
|
+
category: "network",
|
|
402
|
+
check: ["sysctl", "-n", "net.ipv4.tcp_syncookies"],
|
|
403
|
+
expectedOutput: "^1$",
|
|
404
|
+
remediation: ["sysctl", "-w", "net.ipv4.tcp_syncookies=1"],
|
|
405
|
+
references: ["CIS-3.2.8", "NIST-SC-5"],
|
|
406
|
+
},
|
|
407
|
+
{
|
|
408
|
+
id: "AUTH-001",
|
|
409
|
+
title: "Root login via SSH disabled",
|
|
410
|
+
description: "Ensure root login is disabled in SSH configuration",
|
|
411
|
+
severity: "critical",
|
|
412
|
+
category: "authentication",
|
|
413
|
+
check: ["grep", "-i", "^PermitRootLogin", "/etc/ssh/sshd_config"],
|
|
414
|
+
expectedOutput: "PermitRootLogin\\s+no",
|
|
415
|
+
remediation: [
|
|
416
|
+
"sed",
|
|
417
|
+
"-i",
|
|
418
|
+
"s/^#\\?PermitRootLogin.*/PermitRootLogin no/",
|
|
419
|
+
"/etc/ssh/sshd_config",
|
|
420
|
+
],
|
|
421
|
+
references: ["CIS-5.2.10", "NIST-IA-2"],
|
|
422
|
+
},
|
|
423
|
+
{
|
|
424
|
+
id: "AUTH-002",
|
|
425
|
+
title: "Password authentication disabled for SSH",
|
|
426
|
+
description: "Ensure password authentication is disabled in favor of key-based auth",
|
|
427
|
+
severity: "high",
|
|
428
|
+
category: "authentication",
|
|
429
|
+
check: [
|
|
430
|
+
"grep",
|
|
431
|
+
"-i",
|
|
432
|
+
"^PasswordAuthentication",
|
|
433
|
+
"/etc/ssh/sshd_config",
|
|
434
|
+
],
|
|
435
|
+
expectedOutput: "PasswordAuthentication\\s+no",
|
|
436
|
+
references: ["CIS-5.2.12", "NIST-IA-2"],
|
|
437
|
+
},
|
|
438
|
+
{
|
|
439
|
+
id: "FS-001",
|
|
440
|
+
title: "/tmp has noexec mount option",
|
|
441
|
+
description: "Ensure /tmp is mounted with noexec option",
|
|
442
|
+
severity: "medium",
|
|
443
|
+
category: "filesystem",
|
|
444
|
+
check: ["findmnt", "-n", "-o", "OPTIONS", "/tmp"],
|
|
445
|
+
expectedOutput: "noexec",
|
|
446
|
+
references: ["CIS-1.1.4", "NIST-CM-6"],
|
|
447
|
+
},
|
|
448
|
+
{
|
|
449
|
+
id: "FS-002",
|
|
450
|
+
title: "Sticky bit on world-writable directories",
|
|
451
|
+
description: "Ensure sticky bit is set on all world-writable directories",
|
|
452
|
+
severity: "medium",
|
|
453
|
+
category: "filesystem",
|
|
454
|
+
check: [
|
|
455
|
+
"find",
|
|
456
|
+
"/",
|
|
457
|
+
"-xdev",
|
|
458
|
+
"-type",
|
|
459
|
+
"d",
|
|
460
|
+
"-perm",
|
|
461
|
+
"-0002",
|
|
462
|
+
"!",
|
|
463
|
+
"-perm",
|
|
464
|
+
"-1000",
|
|
465
|
+
"-print",
|
|
466
|
+
],
|
|
467
|
+
expectedOutput: "^$",
|
|
468
|
+
references: ["CIS-1.1.21", "NIST-CM-6"],
|
|
469
|
+
},
|
|
470
|
+
{
|
|
471
|
+
id: "SVC-001",
|
|
472
|
+
title: "Firewall service active",
|
|
473
|
+
description: "Ensure a firewall service (iptables/nftables/ufw) is running",
|
|
474
|
+
severity: "critical",
|
|
475
|
+
category: "services",
|
|
476
|
+
check: ["systemctl", "is-active", "ufw"],
|
|
477
|
+
expectedOutput: "^active$",
|
|
478
|
+
references: ["CIS-3.5.1", "NIST-SC-7"],
|
|
479
|
+
},
|
|
480
|
+
{
|
|
481
|
+
id: "SVC-002",
|
|
482
|
+
title: "Auditd service active",
|
|
483
|
+
description: "Ensure the audit daemon is running",
|
|
484
|
+
severity: "high",
|
|
485
|
+
category: "services",
|
|
486
|
+
check: ["systemctl", "is-active", "auditd"],
|
|
487
|
+
expectedOutput: "^active$",
|
|
488
|
+
remediation: ["systemctl", "enable", "--now", "auditd"],
|
|
489
|
+
references: ["CIS-4.1.1.1", "NIST-AU-2"],
|
|
490
|
+
},
|
|
491
|
+
{
|
|
492
|
+
id: "SVC-003",
|
|
493
|
+
title: "Fail2ban service active",
|
|
494
|
+
description: "Ensure fail2ban is running to protect against brute force attacks",
|
|
495
|
+
severity: "high",
|
|
496
|
+
category: "services",
|
|
497
|
+
check: ["systemctl", "is-active", "fail2ban"],
|
|
498
|
+
expectedOutput: "^active$",
|
|
499
|
+
remediation: ["systemctl", "enable", "--now", "fail2ban"],
|
|
500
|
+
references: ["NIST-SI-4"],
|
|
501
|
+
},
|
|
502
|
+
{
|
|
503
|
+
id: "PERM-001",
|
|
504
|
+
title: "No world-writable files in system directories",
|
|
505
|
+
description: "Check for world-writable files in critical system directories",
|
|
506
|
+
severity: "high",
|
|
507
|
+
category: "permissions",
|
|
508
|
+
check: [
|
|
509
|
+
"find",
|
|
510
|
+
"/etc",
|
|
511
|
+
"-xdev",
|
|
512
|
+
"-type",
|
|
513
|
+
"f",
|
|
514
|
+
"-perm",
|
|
515
|
+
"-0002",
|
|
516
|
+
"-print",
|
|
517
|
+
],
|
|
518
|
+
expectedOutput: "^$",
|
|
519
|
+
references: ["CIS-6.1.10", "NIST-CM-6"],
|
|
520
|
+
},
|
|
521
|
+
{
|
|
522
|
+
id: "PERM-002",
|
|
523
|
+
title: "/etc/shadow permissions",
|
|
524
|
+
description: "Ensure /etc/shadow has restrictive permissions",
|
|
525
|
+
severity: "critical",
|
|
526
|
+
category: "permissions",
|
|
527
|
+
check: ["stat", "-c", "%a", "/etc/shadow"],
|
|
528
|
+
expectedOutput: "^(0|600|640)$",
|
|
529
|
+
references: ["CIS-6.1.3", "NIST-AC-3"],
|
|
530
|
+
},
|
|
531
|
+
{
|
|
532
|
+
id: "PERM-003",
|
|
533
|
+
title: "/etc/passwd permissions",
|
|
534
|
+
description: "Ensure /etc/passwd has proper permissions",
|
|
535
|
+
severity: "high",
|
|
536
|
+
category: "permissions",
|
|
537
|
+
check: ["stat", "-c", "%a", "/etc/passwd"],
|
|
538
|
+
expectedOutput: "^644$",
|
|
539
|
+
references: ["CIS-6.1.2", "NIST-AC-3"],
|
|
540
|
+
},
|
|
541
|
+
{
|
|
542
|
+
id: "AUTH-003",
|
|
543
|
+
title: "PAM Password Quality",
|
|
544
|
+
description: "Ensure pam_pwquality is configured with minimum length 14. " +
|
|
545
|
+
"Remediate with: access_pam_configure with module=pwquality",
|
|
546
|
+
severity: "high",
|
|
547
|
+
category: "authentication",
|
|
548
|
+
check: ["grep", "-i", "minlen", "/etc/security/pwquality.conf"],
|
|
549
|
+
expectedOutput: "minlen",
|
|
550
|
+
references: ["CIS-5.3.1", "NIST-IA-5"],
|
|
551
|
+
},
|
|
552
|
+
{
|
|
553
|
+
id: "AUTH-004",
|
|
554
|
+
title: "PAM Account Lockout",
|
|
555
|
+
description: "Ensure pam_faillock is configured for account lockout. " +
|
|
556
|
+
"Remediate with: access_pam_configure with module=faillock",
|
|
557
|
+
severity: "high",
|
|
558
|
+
category: "authentication",
|
|
559
|
+
check: ["grep", "-E", "pam_faillock", "/etc/pam.d/common-auth"],
|
|
560
|
+
expectedOutput: "pam_faillock",
|
|
561
|
+
references: ["CIS-5.3.2", "NIST-AC-7"],
|
|
562
|
+
},
|
|
563
|
+
{
|
|
564
|
+
id: "AUTH-005",
|
|
565
|
+
title: "Password Maximum Age",
|
|
566
|
+
description: "Ensure password expiry is 365 days or less. " +
|
|
567
|
+
"Remediate with: access_password_policy with action=set, max_days=365",
|
|
568
|
+
severity: "medium",
|
|
569
|
+
category: "authentication",
|
|
570
|
+
check: ["grep", "-E", "^PASS_MAX_DAYS", "/etc/login.defs"],
|
|
571
|
+
expectedOutput: "PASS_MAX_DAYS\\s+([1-9]|[1-9][0-9]|[12][0-9]{2}|3[0-5][0-9]|36[0-5])$",
|
|
572
|
+
references: ["CIS-5.4.1.1", "NIST-IA-5"],
|
|
573
|
+
},
|
|
574
|
+
{
|
|
575
|
+
id: "FS-003",
|
|
576
|
+
title: "Default Umask 027",
|
|
577
|
+
description: "Ensure default umask is 027 or more restrictive. " +
|
|
578
|
+
"Remediate with: harden_umask_set with umask_value=027",
|
|
579
|
+
severity: "medium",
|
|
580
|
+
category: "filesystem",
|
|
581
|
+
check: ["grep", "UMASK", "/etc/login.defs"],
|
|
582
|
+
expectedOutput: "UMASK\\s+(027|077)",
|
|
583
|
+
references: ["CIS-5.4.4", "NIST-CM-6"],
|
|
584
|
+
},
|
|
585
|
+
{
|
|
586
|
+
id: "FS-004",
|
|
587
|
+
title: "AIDE File Integrity",
|
|
588
|
+
description: "Ensure AIDE file integrity monitoring is installed. " +
|
|
589
|
+
"Remediate with: defense_install with tool=aide",
|
|
590
|
+
severity: "high",
|
|
591
|
+
category: "filesystem",
|
|
592
|
+
check: ["which", "aide"],
|
|
593
|
+
references: ["CIS-1.3.1", "NIST-SI-7"],
|
|
594
|
+
},
|
|
595
|
+
{
|
|
596
|
+
id: "SVC-004",
|
|
597
|
+
title: "Fail2Ban Active",
|
|
598
|
+
description: "Ensure fail2ban service is running. " +
|
|
599
|
+
"Remediate with: defense_install with tool=fail2ban, then enable the service",
|
|
600
|
+
severity: "high",
|
|
601
|
+
category: "services",
|
|
602
|
+
check: ["systemctl", "is-active", "fail2ban"],
|
|
603
|
+
expectedOutput: "^active$",
|
|
604
|
+
remediation: ["systemctl", "enable", "--now", "fail2ban"],
|
|
605
|
+
references: ["NIST-SI-4"],
|
|
606
|
+
},
|
|
607
|
+
{
|
|
608
|
+
id: "SVC-005",
|
|
609
|
+
title: "Docker Security Options Enabled",
|
|
610
|
+
description: "Ensure Docker daemon has security options (seccomp, apparmor) enabled. " +
|
|
611
|
+
"For per-container privileged mode checks, use: container_docker_bench",
|
|
612
|
+
severity: "critical",
|
|
613
|
+
category: "services",
|
|
614
|
+
check: ["docker", "info", "--format", "{{.SecurityOptions}}"],
|
|
615
|
+
expectedOutput: "seccomp",
|
|
616
|
+
references: ["CIS-Docker-5.4"],
|
|
617
|
+
},
|
|
618
|
+
{
|
|
619
|
+
id: "SVC-006",
|
|
620
|
+
title: "AppArmor Enforcing",
|
|
621
|
+
description: "Ensure AppArmor is enabled and has enforcing profiles. " +
|
|
622
|
+
"Remediate with: container_apparmor_install with action=install_profiles",
|
|
623
|
+
severity: "medium",
|
|
624
|
+
category: "services",
|
|
625
|
+
check: ["aa-enabled"],
|
|
626
|
+
expectedOutput: "^Yes$",
|
|
627
|
+
references: ["CIS-1.6.1.1", "NIST-AC-3"],
|
|
628
|
+
},
|
|
629
|
+
{
|
|
630
|
+
id: "FS-005",
|
|
631
|
+
title: "Core Dumps Disabled",
|
|
632
|
+
description: "Ensure core dumps are restricted. " +
|
|
633
|
+
"Remediate with: harden_coredump_disable",
|
|
634
|
+
severity: "medium",
|
|
635
|
+
category: "filesystem",
|
|
636
|
+
check: [
|
|
637
|
+
"grep",
|
|
638
|
+
"-E",
|
|
639
|
+
"\\*\\s+hard\\s+core\\s+0",
|
|
640
|
+
"/etc/security/limits.conf",
|
|
641
|
+
],
|
|
642
|
+
expectedOutput: "\\*\\s+hard\\s+core\\s+0",
|
|
643
|
+
references: ["CIS-1.5.1", "NIST-CM-6"],
|
|
644
|
+
},
|
|
645
|
+
{
|
|
646
|
+
id: "FS-006",
|
|
647
|
+
title: "/var/tmp noexec Mount",
|
|
648
|
+
description: "Ensure /var/tmp is mounted with noexec option. " +
|
|
649
|
+
"Remediate with: compliance_tmp_hardening with action=apply",
|
|
650
|
+
severity: "medium",
|
|
651
|
+
category: "filesystem",
|
|
652
|
+
check: ["findmnt", "-n", "-o", "OPTIONS", "/var/tmp"],
|
|
653
|
+
expectedOutput: "noexec",
|
|
654
|
+
references: ["CIS-1.1.4", "NIST-CM-6"],
|
|
655
|
+
},
|
|
656
|
+
];
|