clawhatch 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.
Files changed (115) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +348 -0
  3. package/dist/checks/cloud-sync.d.ts +10 -0
  4. package/dist/checks/cloud-sync.d.ts.map +1 -0
  5. package/dist/checks/cloud-sync.js +62 -0
  6. package/dist/checks/cloud-sync.js.map +1 -0
  7. package/dist/checks/data-protection.d.ts +9 -0
  8. package/dist/checks/data-protection.d.ts.map +1 -0
  9. package/dist/checks/data-protection.js +197 -0
  10. package/dist/checks/data-protection.js.map +1 -0
  11. package/dist/checks/identity.d.ts +14 -0
  12. package/dist/checks/identity.d.ts.map +1 -0
  13. package/dist/checks/identity.js +327 -0
  14. package/dist/checks/identity.js.map +1 -0
  15. package/dist/checks/model.d.ts +10 -0
  16. package/dist/checks/model.d.ts.map +1 -0
  17. package/dist/checks/model.js +337 -0
  18. package/dist/checks/model.js.map +1 -0
  19. package/dist/checks/network.d.ts +9 -0
  20. package/dist/checks/network.d.ts.map +1 -0
  21. package/dist/checks/network.js +177 -0
  22. package/dist/checks/network.js.map +1 -0
  23. package/dist/checks/operational.d.ts +9 -0
  24. package/dist/checks/operational.d.ts.map +1 -0
  25. package/dist/checks/operational.js +158 -0
  26. package/dist/checks/operational.js.map +1 -0
  27. package/dist/checks/sandbox.d.ts +9 -0
  28. package/dist/checks/sandbox.d.ts.map +1 -0
  29. package/dist/checks/sandbox.js +135 -0
  30. package/dist/checks/sandbox.js.map +1 -0
  31. package/dist/checks/secrets.d.ts +9 -0
  32. package/dist/checks/secrets.d.ts.map +1 -0
  33. package/dist/checks/secrets.js +816 -0
  34. package/dist/checks/secrets.js.map +1 -0
  35. package/dist/checks/skills.d.ts +9 -0
  36. package/dist/checks/skills.d.ts.map +1 -0
  37. package/dist/checks/skills.js +303 -0
  38. package/dist/checks/skills.js.map +1 -0
  39. package/dist/checks/tools.d.ts +9 -0
  40. package/dist/checks/tools.d.ts.map +1 -0
  41. package/dist/checks/tools.js +397 -0
  42. package/dist/checks/tools.js.map +1 -0
  43. package/dist/discover.d.ts +22 -0
  44. package/dist/discover.d.ts.map +1 -0
  45. package/dist/discover.js +281 -0
  46. package/dist/discover.js.map +1 -0
  47. package/dist/fixer.d.ts +16 -0
  48. package/dist/fixer.d.ts.map +1 -0
  49. package/dist/fixer.js +361 -0
  50. package/dist/fixer.js.map +1 -0
  51. package/dist/index.d.ts +16 -0
  52. package/dist/index.d.ts.map +1 -0
  53. package/dist/index.js +230 -0
  54. package/dist/index.js.map +1 -0
  55. package/dist/init.d.ts +14 -0
  56. package/dist/init.d.ts.map +1 -0
  57. package/dist/init.js +108 -0
  58. package/dist/init.js.map +1 -0
  59. package/dist/notify.d.ts +28 -0
  60. package/dist/notify.d.ts.map +1 -0
  61. package/dist/notify.js +217 -0
  62. package/dist/notify.js.map +1 -0
  63. package/dist/parsers/config.d.ts +16 -0
  64. package/dist/parsers/config.d.ts.map +1 -0
  65. package/dist/parsers/config.js +54 -0
  66. package/dist/parsers/config.js.map +1 -0
  67. package/dist/parsers/env.d.ts +6 -0
  68. package/dist/parsers/env.d.ts.map +1 -0
  69. package/dist/parsers/env.js +35 -0
  70. package/dist/parsers/env.js.map +1 -0
  71. package/dist/parsers/jsonl.d.ts +12 -0
  72. package/dist/parsers/jsonl.d.ts.map +1 -0
  73. package/dist/parsers/jsonl.js +61 -0
  74. package/dist/parsers/jsonl.js.map +1 -0
  75. package/dist/parsers/markdown.d.ts +17 -0
  76. package/dist/parsers/markdown.d.ts.map +1 -0
  77. package/dist/parsers/markdown.js +57 -0
  78. package/dist/parsers/markdown.js.map +1 -0
  79. package/dist/reporter-html.d.ts +9 -0
  80. package/dist/reporter-html.d.ts.map +1 -0
  81. package/dist/reporter-html.js +581 -0
  82. package/dist/reporter-html.js.map +1 -0
  83. package/dist/reporter.d.ts +10 -0
  84. package/dist/reporter.d.ts.map +1 -0
  85. package/dist/reporter.js +133 -0
  86. package/dist/reporter.js.map +1 -0
  87. package/dist/sanitize.d.ts +17 -0
  88. package/dist/sanitize.d.ts.map +1 -0
  89. package/dist/sanitize.js +83 -0
  90. package/dist/sanitize.js.map +1 -0
  91. package/dist/scanner.d.ts +18 -0
  92. package/dist/scanner.d.ts.map +1 -0
  93. package/dist/scanner.js +236 -0
  94. package/dist/scanner.js.map +1 -0
  95. package/dist/scoring.d.ts +17 -0
  96. package/dist/scoring.d.ts.map +1 -0
  97. package/dist/scoring.js +47 -0
  98. package/dist/scoring.js.map +1 -0
  99. package/dist/telemetry.d.ts +16 -0
  100. package/dist/telemetry.d.ts.map +1 -0
  101. package/dist/telemetry.js +52 -0
  102. package/dist/telemetry.js.map +1 -0
  103. package/dist/threat-feed.d.ts +14 -0
  104. package/dist/threat-feed.d.ts.map +1 -0
  105. package/dist/threat-feed.js +133 -0
  106. package/dist/threat-feed.js.map +1 -0
  107. package/dist/types.d.ts +221 -0
  108. package/dist/types.d.ts.map +1 -0
  109. package/dist/types.js +11 -0
  110. package/dist/types.js.map +1 -0
  111. package/dist/utils.d.ts +12 -0
  112. package/dist/utils.d.ts.map +1 -0
  113. package/dist/utils.js +34 -0
  114. package/dist/utils.js.map +1 -0
  115. package/package.json +71 -0
@@ -0,0 +1,281 @@
1
+ /**
2
+ * File discovery for OpenClaw installations.
3
+ *
4
+ * Finds all relevant files to scan, handling:
5
+ * - Default and custom paths
6
+ * - Symlink detection and boundary checking
7
+ * - Windows/macOS/Linux path differences
8
+ * - Graceful missing-file handling
9
+ */
10
+ import { lstat, readlink, readdir, access, constants } from "node:fs/promises";
11
+ import { homedir, platform } from "node:os";
12
+ import { join, resolve } from "node:path";
13
+ import { glob } from "glob";
14
+ /** Common alternative locations for OpenClaw installations */
15
+ function getAlternativePaths() {
16
+ const home = homedir();
17
+ const paths = [join(home, ".openclaw")];
18
+ if (platform() === "win32") {
19
+ const appdata = process.env.APPDATA;
20
+ if (appdata) {
21
+ paths.push(join(appdata, "openclaw"));
22
+ }
23
+ paths.push(join(home, "AppData", "Roaming", "openclaw"));
24
+ }
25
+ return paths;
26
+ }
27
+ /**
28
+ * Resolve ~ to home directory.
29
+ */
30
+ function expandHome(p) {
31
+ if (p.startsWith("~")) {
32
+ return join(homedir(), p.slice(1));
33
+ }
34
+ return p;
35
+ }
36
+ /**
37
+ * Check if a path is a symlink and if it stays within expected boundaries.
38
+ * Returns the resolved real path, or null if it's suspicious.
39
+ */
40
+ async function safeResolvePath(filePath, expectedRoot) {
41
+ try {
42
+ const lstats = await lstat(filePath);
43
+ if (lstats.isSymbolicLink()) {
44
+ const target = await readlink(filePath);
45
+ const resolved = resolve(filePath, "..", target);
46
+ // FIX: Only use case-insensitive comparison on case-insensitive filesystems (Windows/macOS)
47
+ const caseSensitive = platform() === "linux";
48
+ const outsideBoundary = caseSensitive
49
+ ? !resolved.startsWith(expectedRoot)
50
+ : !resolved.toLowerCase().startsWith(expectedRoot.toLowerCase());
51
+ return { path: resolved, isSymlink: true, outsideBoundary };
52
+ }
53
+ return { path: filePath, isSymlink: false, outsideBoundary: false };
54
+ }
55
+ catch {
56
+ return { path: filePath, isSymlink: false, outsideBoundary: false };
57
+ }
58
+ }
59
+ /**
60
+ * Auto-detect OpenClaw installation path.
61
+ */
62
+ export async function findOpenClawDir(customPath) {
63
+ if (customPath) {
64
+ const expanded = expandHome(customPath);
65
+ try {
66
+ await access(expanded, constants.R_OK);
67
+ return expanded;
68
+ }
69
+ catch {
70
+ return null;
71
+ }
72
+ }
73
+ for (const candidate of getAlternativePaths()) {
74
+ try {
75
+ await access(candidate, constants.R_OK);
76
+ return candidate;
77
+ }
78
+ catch {
79
+ continue;
80
+ }
81
+ }
82
+ return null;
83
+ }
84
+ /**
85
+ * Discover all scannable files in an OpenClaw installation.
86
+ */
87
+ export async function discoverFiles(openclawDir, workspaceDir) {
88
+ const symlinkWarnings = [];
89
+ const resolvedRoot = resolve(openclawDir);
90
+ const files = {
91
+ configPath: null,
92
+ envPath: null,
93
+ credentialFiles: [],
94
+ authProfileFiles: [],
95
+ sessionLogFiles: [],
96
+ workspaceMarkdownFiles: [],
97
+ skillFiles: [],
98
+ customCommandFiles: [],
99
+ skillPackageFiles: [],
100
+ privateKeyFiles: [],
101
+ sshKeyFiles: [],
102
+ openclawDir: resolvedRoot,
103
+ workspaceDir: workspaceDir ? resolve(workspaceDir) : null,
104
+ };
105
+ // Config file
106
+ const configCandidate = join(resolvedRoot, "openclaw.json");
107
+ try {
108
+ const resolved = await safeResolvePath(configCandidate, resolvedRoot);
109
+ if (resolved.isSymlink && resolved.outsideBoundary) {
110
+ symlinkWarnings.push(`Symlink: ${configCandidate} -> ${resolved.path} (outside OpenClaw directory)`);
111
+ }
112
+ else {
113
+ await access(resolved.path, constants.R_OK);
114
+ files.configPath = resolved.path;
115
+ }
116
+ }
117
+ catch {
118
+ // No config file
119
+ }
120
+ // .env file
121
+ const envCandidate = join(resolvedRoot, ".env");
122
+ try {
123
+ await access(envCandidate, constants.R_OK);
124
+ files.envPath = envCandidate;
125
+ }
126
+ catch {
127
+ // No .env
128
+ }
129
+ // Credential files
130
+ try {
131
+ const credDir = join(resolvedRoot, "credentials");
132
+ const resolved = await safeResolvePath(credDir, resolvedRoot);
133
+ if (resolved.isSymlink && resolved.outsideBoundary) {
134
+ symlinkWarnings.push(`Symlink: credentials/ -> ${resolved.path} (outside OpenClaw directory)`);
135
+ }
136
+ else {
137
+ try {
138
+ const credFiles = await readdir(credDir);
139
+ for (const f of credFiles) {
140
+ if (f.endsWith(".json")) {
141
+ files.credentialFiles.push(join(credDir, f));
142
+ }
143
+ }
144
+ }
145
+ catch {
146
+ // No credentials dir
147
+ }
148
+ }
149
+ }
150
+ catch {
151
+ // Can't stat
152
+ }
153
+ // Auth profile files (agents/*/auth-profiles.json)
154
+ try {
155
+ const matches = await glob("agents/*/auth-profiles.json", {
156
+ cwd: resolvedRoot,
157
+ absolute: true,
158
+ });
159
+ files.authProfileFiles = matches;
160
+ }
161
+ catch {
162
+ // glob failed
163
+ }
164
+ // Session log files (agents/*/sessions/*.jsonl)
165
+ try {
166
+ const matches = await glob("agents/*/sessions/*.jsonl", {
167
+ cwd: resolvedRoot,
168
+ absolute: true,
169
+ });
170
+ files.sessionLogFiles = matches;
171
+ }
172
+ catch {
173
+ // glob failed
174
+ }
175
+ // Skill files (managed)
176
+ try {
177
+ const matches = await glob("skills/*/SKILL.md", {
178
+ cwd: resolvedRoot,
179
+ absolute: true,
180
+ });
181
+ files.skillFiles = matches;
182
+ }
183
+ catch {
184
+ // glob failed
185
+ }
186
+ // Workspace files
187
+ if (workspaceDir) {
188
+ const wsResolved = resolve(workspaceDir);
189
+ const mdFiles = ["SOUL.md", "AGENTS.md", "TOOLS.md", "MEMORY.md"];
190
+ for (const md of mdFiles) {
191
+ const candidate = join(wsResolved, md);
192
+ try {
193
+ await access(candidate, constants.R_OK);
194
+ files.workspaceMarkdownFiles.push(candidate);
195
+ }
196
+ catch {
197
+ // File doesn't exist
198
+ }
199
+ }
200
+ // memory/*.md
201
+ try {
202
+ const matches = await glob("memory/*.md", {
203
+ cwd: wsResolved,
204
+ absolute: true,
205
+ });
206
+ files.workspaceMarkdownFiles.push(...matches);
207
+ }
208
+ catch {
209
+ // glob failed
210
+ }
211
+ // Workspace skills
212
+ try {
213
+ const matches = await glob("skills/*/SKILL.md", {
214
+ cwd: wsResolved,
215
+ absolute: true,
216
+ });
217
+ files.skillFiles.push(...matches);
218
+ }
219
+ catch {
220
+ // glob failed
221
+ }
222
+ // Custom command files (.claude/commands/*.md)
223
+ try {
224
+ const matches = await glob(".claude/commands/*.md", {
225
+ cwd: wsResolved,
226
+ absolute: true,
227
+ });
228
+ files.customCommandFiles.push(...matches);
229
+ }
230
+ catch {
231
+ // glob failed
232
+ }
233
+ // Skill package.json files (workspace)
234
+ try {
235
+ const matches = await glob("skills/*/package.json", {
236
+ cwd: wsResolved,
237
+ absolute: true,
238
+ });
239
+ files.skillPackageFiles.push(...matches);
240
+ }
241
+ catch {
242
+ // glob failed
243
+ }
244
+ // Private key files
245
+ try {
246
+ const matches = await glob("**/*.{pem,key,p12}", {
247
+ cwd: wsResolved,
248
+ absolute: true,
249
+ maxDepth: 3,
250
+ });
251
+ files.privateKeyFiles.push(...matches);
252
+ }
253
+ catch {
254
+ // glob failed
255
+ }
256
+ // SSH key files
257
+ for (const name of ["id_rsa", "id_ed25519"]) {
258
+ const candidate = join(wsResolved, name);
259
+ try {
260
+ await access(candidate, constants.R_OK);
261
+ files.sshKeyFiles.push(candidate);
262
+ }
263
+ catch {
264
+ // doesn't exist
265
+ }
266
+ }
267
+ }
268
+ // Skill package.json files (openclaw dir)
269
+ try {
270
+ const matches = await glob("skills/*/package.json", {
271
+ cwd: resolvedRoot,
272
+ absolute: true,
273
+ });
274
+ files.skillPackageFiles.push(...matches);
275
+ }
276
+ catch {
277
+ // glob failed
278
+ }
279
+ return { files, symlinkWarnings };
280
+ }
281
+ //# sourceMappingURL=discover.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"discover.js","sourceRoot":"","sources":["../src/discover.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Auto-fix system.
3
+ *
4
+ * Rules:
5
+ * 1. ALWAYS back up before modifying any file (.bak.{timestamp})
6
+ * 2. "safe" fixes: permissions, .gitignore, tokens — applied automatically
7
+ * 3. "behavioral" fixes: dmPolicy, sandbox changes — prompt user first
8
+ * 4. Log all changes made
9
+ */
10
+ import type { Finding, FixResult } from "./types.js";
11
+ /**
12
+ * Apply fixes for all auto-fixable findings.
13
+ * Returns a list of what was fixed and what was skipped.
14
+ */
15
+ export declare function applyFixes(findings: Finding[], configPath: string | null, openclawDir: string): Promise<FixResult[]>;
16
+ //# sourceMappingURL=fixer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fixer.d.ts","sourceRoot":"","sources":["../src/fixer.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AASH,OAAO,KAAK,EAAE,OAAO,EAAE,SAAS,EAAkB,MAAM,YAAY,CAAC;AAuBrE;;;GAGG;AACH,wBAAsB,UAAU,CAC9B,QAAQ,EAAE,OAAO,EAAE,EACnB,UAAU,EAAE,MAAM,GAAG,IAAI,EACzB,WAAW,EAAE,MAAM,GAClB,OAAO,CAAC,SAAS,EAAE,CAAC,CAqEtB"}
package/dist/fixer.js ADDED
@@ -0,0 +1,361 @@
1
+ /**
2
+ * Auto-fix system.
3
+ *
4
+ * Rules:
5
+ * 1. ALWAYS back up before modifying any file (.bak.{timestamp})
6
+ * 2. "safe" fixes: permissions, .gitignore, tokens — applied automatically
7
+ * 3. "behavioral" fixes: dmPolicy, sandbox changes — prompt user first
8
+ * 4. Log all changes made
9
+ */
10
+ import { readFile, writeFile, copyFile, chmod } from "node:fs/promises";
11
+ import { createInterface } from "node:readline";
12
+ import { platform } from "node:os";
13
+ import { join } from "node:path";
14
+ import { randomBytes } from "node:crypto";
15
+ import JSON5 from "json5";
16
+ import chalk from "chalk";
17
+ function backupPath(filePath) {
18
+ const ts = new Date().toISOString().replace(/[:.]/g, "-");
19
+ return `${filePath}.bak.${ts}`;
20
+ }
21
+ async function backupFile(filePath) {
22
+ const dest = backupPath(filePath);
23
+ await copyFile(filePath, dest);
24
+ return dest;
25
+ }
26
+ async function promptUser(message) {
27
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
28
+ return new Promise((resolve) => {
29
+ rl.question(`${message} [y/N] `, (answer) => {
30
+ rl.close();
31
+ resolve(answer.toLowerCase() === "y" || answer.toLowerCase() === "yes");
32
+ });
33
+ });
34
+ }
35
+ /**
36
+ * Apply fixes for all auto-fixable findings.
37
+ * Returns a list of what was fixed and what was skipped.
38
+ */
39
+ export async function applyFixes(findings, configPath, openclawDir) {
40
+ const results = [];
41
+ const fixable = findings.filter((f) => f.autoFixable);
42
+ if (fixable.length === 0) {
43
+ console.log(chalk.dim("\n No auto-fixable issues found.\n"));
44
+ return results;
45
+ }
46
+ console.log(chalk.cyan(`\n Found ${fixable.length} auto-fixable issue(s). Applying fixes...\n`));
47
+ // Group config-related fixes to apply them in a single write
48
+ const configFixes = [];
49
+ const permissionFixes = [];
50
+ const otherFixes = [];
51
+ for (const f of fixable) {
52
+ if (isConfigFix(f)) {
53
+ configFixes.push(f);
54
+ }
55
+ else if (isPermissionFix(f)) {
56
+ permissionFixes.push(f);
57
+ }
58
+ else {
59
+ otherFixes.push(f);
60
+ }
61
+ }
62
+ // Apply config fixes (batch — single backup + write)
63
+ if (configFixes.length > 0 && configPath) {
64
+ const configResults = await applyConfigFixes(configFixes, configPath);
65
+ results.push(...configResults);
66
+ }
67
+ // Apply permission fixes
68
+ for (const f of permissionFixes) {
69
+ const r = await applyPermissionFix(f);
70
+ results.push(r);
71
+ }
72
+ // Apply other fixes (e.g., .gitignore)
73
+ for (const f of otherFixes) {
74
+ if (f.id === "SECRET-002") {
75
+ const r = await applyGitignoreFix(openclawDir);
76
+ results.push(r);
77
+ }
78
+ }
79
+ // Summary
80
+ console.log("");
81
+ const applied = results.filter((r) => r.applied).length;
82
+ const skipped = results.filter((r) => !r.applied).length;
83
+ console.log(chalk.green(` ${applied} fix(es) applied, ${skipped} skipped.`));
84
+ for (const r of results) {
85
+ if (r.applied) {
86
+ console.log(chalk.green(` + ${r.description}`));
87
+ if (r.backupPath) {
88
+ console.log(chalk.dim(` Backup: ${r.backupPath}`));
89
+ }
90
+ }
91
+ else {
92
+ console.log(chalk.dim(` - Skipped: ${r.description} (${r.skippedReason})`));
93
+ }
94
+ }
95
+ console.log("");
96
+ return results;
97
+ }
98
+ function isConfigFix(finding) {
99
+ // FIX: Removed IDENTITY-008 — applyConfigMutation doesn't implement it,
100
+ // so it would silently fail with "Mutation not implemented" message
101
+ return [
102
+ "IDENTITY-001", "IDENTITY-003", "IDENTITY-005", "IDENTITY-007",
103
+ "IDENTITY-009",
104
+ "NETWORK-001", "NETWORK-002", "NETWORK-003", "NETWORK-004",
105
+ "NETWORK-006", "NETWORK-007",
106
+ "SANDBOX-001", "SANDBOX-006", "SANDBOX-007", "SANDBOX-008",
107
+ ].includes(finding.id);
108
+ }
109
+ function isPermissionFix(finding) {
110
+ return [
111
+ "IDENTITY-012",
112
+ "SECRET-003", "SECRET-004", "SECRET-005", "SECRET-006",
113
+ ].includes(finding.id);
114
+ }
115
+ async function applyConfigFixes(fixes, configPath) {
116
+ const results = [];
117
+ // Separate safe vs behavioral
118
+ const safeFixes = fixes.filter((f) => f.fixType === "safe");
119
+ const behavioralFixes = fixes.filter((f) => f.fixType === "behavioral");
120
+ // Prompt for behavioral fixes
121
+ const approvedBehavioral = [];
122
+ for (const f of behavioralFixes) {
123
+ console.log(chalk.yellow(` Behavioral change: ${f.title}`));
124
+ console.log(chalk.dim(` ${f.description}`));
125
+ const approved = await promptUser(chalk.yellow(" Apply this change?"));
126
+ if (approved) {
127
+ approvedBehavioral.push(f);
128
+ }
129
+ else {
130
+ results.push({
131
+ finding: f,
132
+ applied: false,
133
+ description: f.title,
134
+ skippedReason: "User declined behavioral change",
135
+ });
136
+ }
137
+ }
138
+ const allApproved = [...safeFixes, ...approvedBehavioral];
139
+ if (allApproved.length === 0)
140
+ return results;
141
+ // Backup config file
142
+ const bak = await backupFile(configPath);
143
+ console.log(chalk.dim(` Backup created: ${bak}`));
144
+ // Read and parse config
145
+ const raw = await readFile(configPath, "utf-8");
146
+ let config;
147
+ try {
148
+ config = JSON5.parse(raw);
149
+ }
150
+ catch {
151
+ results.push({
152
+ finding: allApproved[0],
153
+ applied: false,
154
+ description: "Config file fixes",
155
+ skippedReason: "Could not parse openclaw.json",
156
+ });
157
+ return results;
158
+ }
159
+ // Apply each fix
160
+ for (const f of allApproved) {
161
+ const applied = applyConfigMutation(config, f);
162
+ results.push({
163
+ finding: f,
164
+ applied,
165
+ backupPath: bak,
166
+ description: f.title,
167
+ skippedReason: applied ? undefined : "Mutation not implemented for this check",
168
+ });
169
+ }
170
+ // Write updated config
171
+ await writeFile(configPath, JSON.stringify(config, null, 2), "utf-8");
172
+ return results;
173
+ }
174
+ function applyConfigMutation(config, finding) {
175
+ switch (finding.id) {
176
+ // Network fixes
177
+ case "NETWORK-001": {
178
+ if (!config.gateway)
179
+ config.gateway = {};
180
+ config.gateway.bind = "127.0.0.1";
181
+ return true;
182
+ }
183
+ case "NETWORK-002": {
184
+ if (!config.gateway)
185
+ config.gateway = {};
186
+ if (!config.gateway.auth)
187
+ config.gateway.auth = {};
188
+ config.gateway.auth.mode = "token";
189
+ config.gateway.auth.token = randomBytes(32).toString("hex");
190
+ return true;
191
+ }
192
+ case "NETWORK-003":
193
+ case "NETWORK-004": {
194
+ if (!config.gateway)
195
+ config.gateway = {};
196
+ if (!config.gateway.auth)
197
+ config.gateway.auth = {};
198
+ config.gateway.auth.token = randomBytes(32).toString("hex");
199
+ return true;
200
+ }
201
+ case "NETWORK-006": {
202
+ if (!config.gateway)
203
+ config.gateway = {};
204
+ config.gateway.allowInsecureAuth = false;
205
+ return true;
206
+ }
207
+ case "NETWORK-007": {
208
+ if (!config.gateway)
209
+ config.gateway = {};
210
+ config.gateway.dangerouslyDisableDeviceAuth = false;
211
+ return true;
212
+ }
213
+ // Identity fixes (behavioral — already prompted)
214
+ case "IDENTITY-001": {
215
+ // Set dmPolicy to "pairing" for matching channel
216
+ if (config.channels) {
217
+ for (const ch of Object.values(config.channels)) {
218
+ if (ch.dmPolicy === "open") {
219
+ ch.dmPolicy = "pairing";
220
+ }
221
+ }
222
+ }
223
+ return true;
224
+ }
225
+ case "IDENTITY-003": {
226
+ if (config.channels) {
227
+ for (const ch of Object.values(config.channels)) {
228
+ if (ch.groupPolicy === "open") {
229
+ ch.groupPolicy = "allowlist";
230
+ }
231
+ }
232
+ }
233
+ return true;
234
+ }
235
+ case "IDENTITY-005": {
236
+ if (config.channels) {
237
+ for (const ch of Object.values(config.channels)) {
238
+ if (ch.requireMention === false) {
239
+ ch.requireMention = true;
240
+ }
241
+ }
242
+ }
243
+ return true;
244
+ }
245
+ case "IDENTITY-007": {
246
+ if (!config.pairing)
247
+ config.pairing = {};
248
+ config.pairing.storeTTL = 86400; // 24 hours
249
+ return true;
250
+ }
251
+ case "IDENTITY-009": {
252
+ if (!config.commands)
253
+ config.commands = {};
254
+ config.commands.useAccessGroups = true;
255
+ return true;
256
+ }
257
+ // Sandbox fixes
258
+ case "SANDBOX-001": {
259
+ if (!config.sandbox)
260
+ config.sandbox = {};
261
+ config.sandbox.mode = "all";
262
+ return true;
263
+ }
264
+ case "SANDBOX-006": {
265
+ if (config.sandbox?.docker) {
266
+ config.sandbox.docker.network = "none";
267
+ }
268
+ return true;
269
+ }
270
+ case "SANDBOX-007": {
271
+ if (config.sandbox?.docker) {
272
+ config.sandbox.docker.socketMounted = false;
273
+ }
274
+ return true;
275
+ }
276
+ case "SANDBOX-008": {
277
+ if (config.sandbox?.browser) {
278
+ config.sandbox.browser.allowHostControl = false;
279
+ }
280
+ return true;
281
+ }
282
+ default:
283
+ return false;
284
+ }
285
+ }
286
+ async function applyPermissionFix(finding) {
287
+ if (platform() === "win32") {
288
+ return {
289
+ finding,
290
+ applied: false,
291
+ description: finding.title,
292
+ skippedReason: "File permission fixes require Unix (use Windows Security settings)",
293
+ };
294
+ }
295
+ if (!finding.file) {
296
+ return {
297
+ finding,
298
+ applied: false,
299
+ description: finding.title,
300
+ skippedReason: "No file path in finding",
301
+ };
302
+ }
303
+ try {
304
+ const mode = finding.id === "SECRET-003" ? 0o700 : 0o600;
305
+ await chmod(finding.file, mode);
306
+ return {
307
+ finding,
308
+ applied: true,
309
+ description: `Set ${finding.file} to ${mode.toString(8)}`,
310
+ };
311
+ }
312
+ catch (err) {
313
+ return {
314
+ finding,
315
+ applied: false,
316
+ description: finding.title,
317
+ skippedReason: `chmod failed: ${err instanceof Error ? err.message : "unknown error"}`,
318
+ };
319
+ }
320
+ }
321
+ async function applyGitignoreFix(openclawDir) {
322
+ // FIX: Removed redundant dynamic import — join is now imported at top level
323
+ const gitignorePath = join(openclawDir, ".gitignore");
324
+ try {
325
+ let content = "";
326
+ try {
327
+ content = await readFile(gitignorePath, "utf-8");
328
+ }
329
+ catch {
330
+ // File doesn't exist — create it
331
+ }
332
+ const bak = content ? await backupFile(gitignorePath) : undefined;
333
+ const entries = [".env", ".env.*", "credentials/", "*.key", "*.pem"];
334
+ const missing = entries.filter((e) => !content.includes(e));
335
+ if (missing.length === 0) {
336
+ return {
337
+ finding: { id: "SECRET-002" },
338
+ applied: false,
339
+ description: "Update .gitignore",
340
+ skippedReason: "All entries already present",
341
+ };
342
+ }
343
+ const addition = `\n# Added by Clawhatch security scanner\n${missing.join("\n")}\n`;
344
+ await writeFile(gitignorePath, content + addition, "utf-8");
345
+ return {
346
+ finding: { id: "SECRET-002" },
347
+ applied: true,
348
+ backupPath: bak,
349
+ description: `Added ${missing.length} entries to .gitignore`,
350
+ };
351
+ }
352
+ catch (err) {
353
+ return {
354
+ finding: { id: "SECRET-002" },
355
+ applied: false,
356
+ description: "Update .gitignore",
357
+ skippedReason: `Failed: ${err instanceof Error ? err.message : "unknown error"}`,
358
+ };
359
+ }
360
+ }
361
+ //# sourceMappingURL=fixer.js.map