@token-security/clawdit 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 (94) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +197 -0
  3. package/dist/checks/auth-001-device-auth-disabled.d.ts +10 -0
  4. package/dist/checks/auth-001-device-auth-disabled.js +34 -0
  5. package/dist/checks/auth-002-insecure-fallback.d.ts +10 -0
  6. package/dist/checks/auth-002-insecure-fallback.js +34 -0
  7. package/dist/checks/auth-003-no-gateway-auth.d.ts +10 -0
  8. package/dist/checks/auth-003-no-gateway-auth.js +40 -0
  9. package/dist/checks/auth-004-public-trusted-proxies.d.ts +10 -0
  10. package/dist/checks/auth-004-public-trusted-proxies.js +37 -0
  11. package/dist/checks/auth-005-hooks-no-token.d.ts +10 -0
  12. package/dist/checks/auth-005-hooks-no-token.js +42 -0
  13. package/dist/checks/auth-006-pairing-exposed.d.ts +10 -0
  14. package/dist/checks/auth-006-pairing-exposed.js +46 -0
  15. package/dist/checks/auth-007-missing-trusted-proxies.d.ts +11 -0
  16. package/dist/checks/auth-007-missing-trusted-proxies.js +46 -0
  17. package/dist/checks/chan-001-open-dm.d.ts +10 -0
  18. package/dist/checks/chan-001-open-dm.js +48 -0
  19. package/dist/checks/chan-002-group-policy.d.ts +10 -0
  20. package/dist/checks/chan-002-group-policy.js +43 -0
  21. package/dist/checks/chan-003-no-mention.d.ts +10 -0
  22. package/dist/checks/chan-003-no-mention.js +45 -0
  23. package/dist/checks/chan-004-dm-isolation.d.ts +10 -0
  24. package/dist/checks/chan-004-dm-isolation.js +50 -0
  25. package/dist/checks/chan-005-verbose-groups.d.ts +10 -0
  26. package/dist/checks/chan-005-verbose-groups.js +53 -0
  27. package/dist/checks/disc-001-mdns-full.d.ts +10 -0
  28. package/dist/checks/disc-001-mdns-full.js +34 -0
  29. package/dist/checks/disc-002-mdns-enabled.d.ts +10 -0
  30. package/dist/checks/disc-002-mdns-enabled.js +35 -0
  31. package/dist/checks/exec-001-full-security.d.ts +10 -0
  32. package/dist/checks/exec-001-full-security.js +34 -0
  33. package/dist/checks/exec-002-sandbox-disabled.d.ts +10 -0
  34. package/dist/checks/exec-002-sandbox-disabled.js +34 -0
  35. package/dist/checks/exec-003-elevated-unrestricted.d.ts +10 -0
  36. package/dist/checks/exec-003-elevated-unrestricted.js +38 -0
  37. package/dist/checks/exec-004-approval-fallback.d.ts +10 -0
  38. package/dist/checks/exec-004-approval-fallback.js +50 -0
  39. package/dist/checks/exec-005-sandbox-non-main.d.ts +10 -0
  40. package/dist/checks/exec-005-sandbox-non-main.js +34 -0
  41. package/dist/checks/exec-006-cross-agent-sandbox.d.ts +10 -0
  42. package/dist/checks/exec-006-cross-agent-sandbox.js +34 -0
  43. package/dist/checks/exec-007-workspace-rw.d.ts +10 -0
  44. package/dist/checks/exec-007-workspace-rw.js +34 -0
  45. package/dist/checks/index.d.ts +16 -0
  46. package/dist/checks/index.js +94 -0
  47. package/dist/checks/loader.d.ts +38 -0
  48. package/dist/checks/loader.js +149 -0
  49. package/dist/checks/model-001-weak-model-tools.d.ts +10 -0
  50. package/dist/checks/model-001-weak-model-tools.js +68 -0
  51. package/dist/checks/net-001-gateway-binding.d.ts +10 -0
  52. package/dist/checks/net-001-gateway-binding.js +34 -0
  53. package/dist/checks/net-002-default-port.d.ts +10 -0
  54. package/dist/checks/net-002-default-port.js +35 -0
  55. package/dist/checks/net-003-tailnet-no-token.d.ts +10 -0
  56. package/dist/checks/net-003-tailnet-no-token.js +34 -0
  57. package/dist/checks/plug-001-no-allowlist.d.ts +10 -0
  58. package/dist/checks/plug-001-no-allowlist.js +52 -0
  59. package/dist/checks/plug-002-extensions-exposed.d.ts +10 -0
  60. package/dist/checks/plug-002-extensions-exposed.js +41 -0
  61. package/dist/checks/runner.d.ts +14 -0
  62. package/dist/checks/runner.js +72 -0
  63. package/dist/checks/schema.d.ts +54 -0
  64. package/dist/checks/schema.js +171 -0
  65. package/dist/checks/sec-001-hardcoded-keys.d.ts +10 -0
  66. package/dist/checks/sec-001-hardcoded-keys.js +34 -0
  67. package/dist/checks/sec-002-world-readable-config.d.ts +10 -0
  68. package/dist/checks/sec-002-world-readable-config.js +39 -0
  69. package/dist/checks/sec-003-credentials-exposed.d.ts +10 -0
  70. package/dist/checks/sec-003-credentials-exposed.js +41 -0
  71. package/dist/checks/sec-004-env-readable.d.ts +10 -0
  72. package/dist/checks/sec-004-env-readable.js +40 -0
  73. package/dist/checks/sec-005-transcripts-exposed.d.ts +11 -0
  74. package/dist/checks/sec-005-transcripts-exposed.js +62 -0
  75. package/dist/checks/sec-006-redaction-disabled.d.ts +10 -0
  76. package/dist/checks/sec-006-redaction-disabled.js +34 -0
  77. package/dist/checks/sec-007-no-redact-patterns.d.ts +10 -0
  78. package/dist/checks/sec-007-no-redact-patterns.js +39 -0
  79. package/dist/checks/sec-008-state-dir-permissions.d.ts +10 -0
  80. package/dist/checks/sec-008-state-dir-permissions.js +49 -0
  81. package/dist/checks/types.d.ts +45 -0
  82. package/dist/checks/types.js +2 -0
  83. package/dist/clawdit-output.schema.json +162 -0
  84. package/dist/cli.d.ts +23 -0
  85. package/dist/cli.js +132 -0
  86. package/dist/config.d.ts +22 -0
  87. package/dist/config.js +150 -0
  88. package/dist/formatter.d.ts +42 -0
  89. package/dist/formatter.js +233 -0
  90. package/dist/index.d.ts +3 -0
  91. package/dist/index.js +168 -0
  92. package/dist/utils.d.ts +46 -0
  93. package/dist/utils.js +146 -0
  94. package/package.json +48 -0
@@ -0,0 +1,233 @@
1
+ // ANSI color codes
2
+ const colors = {
3
+ reset: '\x1b[0m',
4
+ red: '\x1b[31m',
5
+ yellow: '\x1b[33m',
6
+ cyan: '\x1b[36m',
7
+ green: '\x1b[32m',
8
+ bold: '\x1b[1m',
9
+ dim: '\x1b[2m',
10
+ };
11
+ function color(text, colorCode, noColor) {
12
+ if (noColor)
13
+ return text;
14
+ return `${colorCode}${text}${colors.reset}`;
15
+ }
16
+ function severityColor(severity, noColor) {
17
+ if (noColor)
18
+ return severity;
19
+ switch (severity) {
20
+ case 'HIGH': return color(severity, colors.red + colors.bold, false);
21
+ case 'MEDIUM': return color(severity, colors.yellow, false);
22
+ case 'LOW': return color(severity, colors.cyan, false);
23
+ }
24
+ }
25
+ /**
26
+ * Format audit result as text, split into parts for proper stream separation
27
+ */
28
+ export function formatText(result, options) {
29
+ const bannerLines = [];
30
+ const findingsLines = [];
31
+ const summaryLines = [];
32
+ const { noColor, quiet, configPath } = options;
33
+ // Banner (unless quiet)
34
+ if (!quiet) {
35
+ const timestamp = new Date().toISOString();
36
+ bannerLines.push('╭──────────────────────────────────────────────────────────────╮');
37
+ bannerLines.push('│ clawdit v0.1.0 - OpenClaw Security Audit │');
38
+ bannerLines.push(`│ Config: ${configPath.padEnd(51)}│`);
39
+ bannerLines.push(`│ Time: ${timestamp.padEnd(53)}│`);
40
+ bannerLines.push('╰──────────────────────────────────────────────────────────────╯');
41
+ bannerLines.push('');
42
+ }
43
+ // Findings
44
+ if (result.findings.length === 0) {
45
+ if (!quiet) {
46
+ findingsLines.push(color('✓ All security checks passed', colors.green, noColor));
47
+ findingsLines.push('');
48
+ }
49
+ }
50
+ else {
51
+ for (const finding of result.findings) {
52
+ findingsLines.push(formatFinding(finding, noColor));
53
+ findingsLines.push('');
54
+ }
55
+ }
56
+ // Summary (unless quiet)
57
+ if (!quiet) {
58
+ summaryLines.push('────────────────────────────────────────────────────────────────');
59
+ const summaryParts = [
60
+ `${result.summary.high} ${severityColor('HIGH', noColor)}`,
61
+ `${result.summary.medium} MEDIUM`,
62
+ `${result.summary.low} LOW`,
63
+ ];
64
+ summaryLines.push(`Summary: ${summaryParts.join(' | ')}`);
65
+ if (result.summary.result === 'PASS') {
66
+ summaryLines.push(`Result: ${color('PASS', colors.green, noColor)} - All security checks passed`);
67
+ }
68
+ else if (result.summary.high > 0) {
69
+ summaryLines.push(`Result: ${color('FAIL', colors.red, noColor)} - Remediate HIGH severity findings before deployment`);
70
+ }
71
+ else if (result.summary.medium > 0) {
72
+ summaryLines.push(`Result: ${color('FAIL', colors.yellow, noColor)} - Review MEDIUM severity findings before deployment`);
73
+ }
74
+ else {
75
+ summaryLines.push(`Result: ${color('FAIL', colors.cyan, noColor)} - Consider remediating LOW severity findings`);
76
+ }
77
+ }
78
+ return {
79
+ banner: bannerLines.join('\n'),
80
+ findings: findingsLines.join('\n'),
81
+ summary: summaryLines.join('\n'),
82
+ };
83
+ }
84
+ /**
85
+ * Format audit result as text, joined as a single string.
86
+ * Used for file output where stream separation is not needed.
87
+ */
88
+ export function formatTextFull(result, options) {
89
+ const parts = formatText(result, options);
90
+ return [parts.banner, parts.findings, parts.summary].filter(Boolean).join('\n');
91
+ }
92
+ function formatFinding(finding, noColor) {
93
+ const lines = [];
94
+ // Header
95
+ lines.push(`[${severityColor(finding.severity, noColor)}] ${finding.id}: ${finding.name}`);
96
+ // Location
97
+ if (finding.location.path) {
98
+ lines.push(` Location: ${finding.location.path}`);
99
+ }
100
+ else {
101
+ lines.push(` Location: ${finding.location.file}`);
102
+ }
103
+ // Values
104
+ lines.push(` Current: ${formatValue(finding.currentValue)}`);
105
+ lines.push(` Expected: ${formatValue(finding.expectedValue)}`);
106
+ lines.push('');
107
+ // Risk (indented, wrapped)
108
+ lines.push(' Risk: ' + finding.risk.split('\n').join('\n '));
109
+ lines.push('');
110
+ // Fix
111
+ lines.push(' Fix:');
112
+ lines.push(` ${color('#', colors.dim, noColor)} ${finding.fix.description}`);
113
+ for (const cmdLine of finding.fix.command.split('\n')) {
114
+ lines.push(` ${cmdLine}`);
115
+ }
116
+ // References
117
+ if (finding.references.length > 0) {
118
+ lines.push('');
119
+ lines.push(' References:');
120
+ for (const ref of finding.references) {
121
+ lines.push(` - ${ref}`);
122
+ }
123
+ }
124
+ return lines.join('\n');
125
+ }
126
+ function formatValue(value) {
127
+ if (value === null)
128
+ return 'null';
129
+ if (value === undefined)
130
+ return 'not set';
131
+ if (typeof value === 'string')
132
+ return value;
133
+ if (typeof value === 'boolean')
134
+ return String(value);
135
+ if (typeof value === 'number')
136
+ return String(value);
137
+ if (Array.isArray(value))
138
+ return JSON.stringify(value);
139
+ if (typeof value === 'object')
140
+ return JSON.stringify(value);
141
+ return String(value);
142
+ }
143
+ /**
144
+ * Format audit result as SARIF 2.1.0
145
+ */
146
+ export function formatSarif(result, options) {
147
+ const severityToLevel = {
148
+ HIGH: 'error',
149
+ MEDIUM: 'warning',
150
+ LOW: 'note',
151
+ };
152
+ // Build unique rules from findings (deduplicate by id)
153
+ const seenRuleIds = new Set();
154
+ const rules = [];
155
+ for (const f of result.findings) {
156
+ if (seenRuleIds.has(f.id))
157
+ continue;
158
+ seenRuleIds.add(f.id);
159
+ rules.push({
160
+ id: f.id,
161
+ name: f.name,
162
+ shortDescription: { text: f.name },
163
+ fullDescription: { text: f.risk },
164
+ help: {
165
+ text: f.fix.description,
166
+ markdown: `**Fix:** ${f.fix.description}\n\n\`\`\`bash\n${f.fix.command}\n\`\`\``,
167
+ },
168
+ helpUri: f.references[0] || undefined,
169
+ defaultConfiguration: { level: severityToLevel[f.severity] },
170
+ });
171
+ }
172
+ // Build results
173
+ const results = result.findings.map((f) => ({
174
+ ruleId: f.id,
175
+ level: severityToLevel[f.severity],
176
+ message: { text: f.risk },
177
+ locations: [{
178
+ physicalLocation: {
179
+ artifactLocation: { uri: f.location.file },
180
+ },
181
+ ...(f.location.path ? { logicalLocations: [{ name: f.location.path }] } : {}),
182
+ }],
183
+ properties: {
184
+ currentValue: f.currentValue,
185
+ expectedValue: f.expectedValue,
186
+ },
187
+ }));
188
+ const sarif = {
189
+ $schema: 'https://json.schemastore.org/sarif-2.1.0.json',
190
+ version: '2.1.0',
191
+ runs: [{
192
+ tool: {
193
+ driver: {
194
+ name: 'clawdit',
195
+ version: '0.1.0',
196
+ informationUri: 'https://github.com/token-security/clawdit',
197
+ rules,
198
+ },
199
+ },
200
+ results,
201
+ invocations: [{
202
+ executionSuccessful: true,
203
+ endTimeUtc: new Date().toISOString(),
204
+ }],
205
+ }],
206
+ };
207
+ return JSON.stringify(sarif, null, 2);
208
+ }
209
+ /**
210
+ * Format audit result as JSON
211
+ */
212
+ export function formatJson(result, options) {
213
+ const output = {
214
+ version: '0.1.0',
215
+ timestamp: new Date().toISOString(),
216
+ config_path: options.configPath,
217
+ summary: result.summary,
218
+ findings: result.findings.map((f) => ({
219
+ id: f.id,
220
+ severity: f.severity,
221
+ name: f.name,
222
+ location: f.location,
223
+ current_value: f.currentValue,
224
+ expected_value: f.expectedValue,
225
+ risk: f.risk,
226
+ fix: f.fix,
227
+ references: f.references,
228
+ })),
229
+ checks_passed: result.passed,
230
+ };
231
+ return JSON.stringify(output, null, 2);
232
+ }
233
+ //# sourceMappingURL=formatter.js.map
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ export {};
3
+ //# sourceMappingURL=index.d.ts.map
package/dist/index.js ADDED
@@ -0,0 +1,168 @@
1
+ #!/usr/bin/env node
2
+ import { writeFileSync } from 'node:fs';
3
+ import { dirname } from 'node:path';
4
+ import { parseCliArgs, printHelp, printVersion, printListChecksText, printListChecksJson, validateCheckIds } from './cli.js';
5
+ import { discoverConfig, parseConfig, parseConfigFromString, readStdinSync, ConfigError } from './config.js';
6
+ import { checks } from './checks/index.js';
7
+ import { runChecks, getExitCode } from './checks/runner.js';
8
+ import { formatText, formatJson, formatSarif } from './formatter.js';
9
+ // Error code to type mapping for structured JSON errors
10
+ const ERROR_TYPES = {
11
+ 10: 'CONFIG_NOT_FOUND',
12
+ 11: 'CONFIG_PARSE_ERROR',
13
+ 12: 'INVALID_ARGUMENTS',
14
+ 13: 'PERMISSION_DENIED',
15
+ 14: 'CIRCULAR_INCLUDE',
16
+ 20: 'INTERNAL_ERROR',
17
+ };
18
+ function outputError(code, message, format, details) {
19
+ if (format === 'json') {
20
+ const errorObj = {
21
+ error: {
22
+ code,
23
+ type: ERROR_TYPES[code] || 'UNKNOWN_ERROR',
24
+ message,
25
+ },
26
+ };
27
+ if (details) {
28
+ errorObj.error.details = details;
29
+ }
30
+ console.log(JSON.stringify(errorObj, null, 2));
31
+ }
32
+ else {
33
+ console.error(`ERROR [E${code}]: ${message}`);
34
+ if (details) {
35
+ console.error('');
36
+ console.error(details);
37
+ }
38
+ }
39
+ }
40
+ async function main() {
41
+ const opts = parseCliArgs(process.argv.slice(2));
42
+ // Handle --help and --version
43
+ if (opts.help) {
44
+ printHelp();
45
+ process.exit(0);
46
+ }
47
+ if (opts.version) {
48
+ printVersion();
49
+ process.exit(0);
50
+ }
51
+ if (opts.listChecks) {
52
+ if (opts.format === 'json') {
53
+ printListChecksJson(checks);
54
+ }
55
+ else {
56
+ printListChecksText(checks, opts.noColor);
57
+ }
58
+ process.exit(0);
59
+ }
60
+ try {
61
+ // Discover and parse config
62
+ let config;
63
+ let configPath;
64
+ let configDir;
65
+ if (opts.configPath === '-') {
66
+ // Read from stdin
67
+ const content = readStdinSync();
68
+ config = parseConfigFromString(content, process.cwd());
69
+ configPath = '<stdin>';
70
+ configDir = process.cwd();
71
+ }
72
+ else {
73
+ // Normal file-based flow
74
+ configPath = discoverConfig(opts.configPath);
75
+ config = parseConfig(configPath);
76
+ configDir = dirname(configPath);
77
+ }
78
+ // Create check context
79
+ const ctx = {
80
+ config,
81
+ configPath,
82
+ configDir,
83
+ };
84
+ // Validate and filter checks
85
+ const allCheckIds = checks.map(c => c.id);
86
+ if (opts.checks) {
87
+ const unknownIds = validateCheckIds(opts.checks, allCheckIds);
88
+ if (unknownIds.length > 0) {
89
+ outputError(12, `Unknown check IDs: ${unknownIds.join(', ')}`, opts.format);
90
+ process.exit(12);
91
+ }
92
+ }
93
+ if (opts.skipChecks) {
94
+ const unknownIds = validateCheckIds(opts.skipChecks, allCheckIds);
95
+ if (unknownIds.length > 0) {
96
+ outputError(12, `Unknown check IDs: ${unknownIds.join(', ')}`, opts.format);
97
+ process.exit(12);
98
+ }
99
+ }
100
+ let checksToRun = checks;
101
+ if (opts.checks) {
102
+ const includeSet = new Set(opts.checks);
103
+ checksToRun = checksToRun.filter(c => includeSet.has(c.id));
104
+ }
105
+ if (opts.skipChecks) {
106
+ const excludeSet = new Set(opts.skipChecks);
107
+ checksToRun = checksToRun.filter(c => !excludeSet.has(c.id));
108
+ }
109
+ // Run checks
110
+ const result = runChecks(checksToRun, ctx, {
111
+ minSeverity: opts.severity,
112
+ onCheckComplete: opts.verbose
113
+ ? (id, passed) => console.error(`Checking ${id}... ${passed ? 'PASS' : 'FAIL'}`)
114
+ : undefined,
115
+ });
116
+ // Format and write output
117
+ if (opts.format === 'text') {
118
+ const parts = formatText(result, {
119
+ noColor: opts.noColor,
120
+ quiet: opts.quiet,
121
+ configPath,
122
+ });
123
+ if (opts.outputFile) {
124
+ // File output: write everything to file
125
+ const fullOutput = [parts.banner, parts.findings, parts.summary].filter(Boolean).join('\n');
126
+ writeFileSync(opts.outputFile, fullOutput + '\n');
127
+ }
128
+ else {
129
+ // Console output: banner/summary to stderr, findings to stdout
130
+ if (parts.banner)
131
+ console.error(parts.banner);
132
+ if (parts.findings)
133
+ console.log(parts.findings);
134
+ if (parts.summary)
135
+ console.error(parts.summary);
136
+ }
137
+ }
138
+ else {
139
+ // JSON/SARIF: all to stdout
140
+ let output;
141
+ if (opts.format === 'json') {
142
+ output = formatJson(result, { configPath });
143
+ }
144
+ else {
145
+ output = formatSarif(result, { configPath });
146
+ }
147
+ if (opts.outputFile) {
148
+ writeFileSync(opts.outputFile, output + '\n');
149
+ }
150
+ else {
151
+ console.log(output);
152
+ }
153
+ }
154
+ // Exit with appropriate code
155
+ process.exit(getExitCode(result));
156
+ }
157
+ catch (error) {
158
+ if (error instanceof ConfigError) {
159
+ outputError(error.code, error.message, opts.format, error.details);
160
+ process.exit(error.code);
161
+ }
162
+ // Unexpected error
163
+ outputError(20, 'Internal error', opts.format, error.message);
164
+ process.exit(20);
165
+ }
166
+ }
167
+ main();
168
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Get a value from an object using dot notation path
3
+ */
4
+ export declare function getValueAtPath(obj: unknown, path: string): unknown;
5
+ /**
6
+ * Set a value in an object using dot notation path
7
+ */
8
+ export declare function setValueAtPath(obj: Record<string, unknown>, path: string, value: unknown): void;
9
+ /**
10
+ * Get file mode (permissions) as octal number. Returns null on Windows.
11
+ */
12
+ export declare function getFileMode(filePath: string): number | null;
13
+ /**
14
+ * Format file mode as octal string (e.g., "644")
15
+ */
16
+ export declare function formatMode(mode: number): string;
17
+ /**
18
+ * Redact sensitive values like API keys
19
+ */
20
+ export declare function redactValue(value: unknown): string;
21
+ /**
22
+ * Check if an IP address is private (RFC1918 or RFC6598 CGNAT)
23
+ */
24
+ export declare function isPrivateIP(ip: string): boolean;
25
+ /**
26
+ * Check if gateway binding requires authentication
27
+ * Returns true if auth is configured
28
+ */
29
+ export declare function hasGatewayAuth(config: Record<string, unknown>): boolean;
30
+ /**
31
+ * API key patterns to detect
32
+ */
33
+ export declare const API_KEY_PATTERNS: Array<{
34
+ key: string;
35
+ pattern: RegExp;
36
+ name: string;
37
+ }>;
38
+ /**
39
+ * Find hardcoded API keys in config
40
+ */
41
+ export declare function findHardcodedApiKeys(config: Record<string, unknown>): Array<{
42
+ key: string;
43
+ value: string;
44
+ name: string;
45
+ }>;
46
+ //# sourceMappingURL=utils.d.ts.map
package/dist/utils.js ADDED
@@ -0,0 +1,146 @@
1
+ import { statSync } from 'node:fs';
2
+ import { platform } from 'node:os';
3
+ /**
4
+ * Get a value from an object using dot notation path
5
+ */
6
+ export function getValueAtPath(obj, path) {
7
+ if (obj === null || obj === undefined)
8
+ return undefined;
9
+ const parts = path.split('.');
10
+ let current = obj;
11
+ for (const part of parts) {
12
+ if (current === null || current === undefined)
13
+ return undefined;
14
+ if (typeof current !== 'object')
15
+ return undefined;
16
+ current = current[part];
17
+ }
18
+ return current;
19
+ }
20
+ /**
21
+ * Set a value in an object using dot notation path
22
+ */
23
+ export function setValueAtPath(obj, path, value) {
24
+ const parts = path.split('.');
25
+ let current = obj;
26
+ for (let i = 0; i < parts.length - 1; i++) {
27
+ const part = parts[i];
28
+ if (!(part in current) || typeof current[part] !== 'object' || current[part] === null) {
29
+ current[part] = {};
30
+ }
31
+ current = current[part];
32
+ }
33
+ current[parts[parts.length - 1]] = value;
34
+ }
35
+ /**
36
+ * Get file mode (permissions) as octal number. Returns null on Windows.
37
+ */
38
+ export function getFileMode(filePath) {
39
+ if (platform() === 'win32')
40
+ return null;
41
+ try {
42
+ const stats = statSync(filePath);
43
+ // Extract permission bits (last 9 bits)
44
+ return stats.mode & 0o777;
45
+ }
46
+ catch {
47
+ return null;
48
+ }
49
+ }
50
+ /**
51
+ * Format file mode as octal string (e.g., "644")
52
+ */
53
+ export function formatMode(mode) {
54
+ return mode.toString(8).padStart(3, '0');
55
+ }
56
+ /**
57
+ * Redact sensitive values like API keys
58
+ */
59
+ export function redactValue(value) {
60
+ if (typeof value !== 'string')
61
+ return String(value);
62
+ // API key patterns
63
+ if (value.startsWith('sk-ant-'))
64
+ return 'sk-ant-***';
65
+ if (value.startsWith('sk-'))
66
+ return 'sk-***';
67
+ if (value.startsWith('r8_'))
68
+ return 'r8_***';
69
+ if (value.startsWith('hf_'))
70
+ return 'hf_***';
71
+ if (/^[A-Za-z0-9_-]{20,}$/.test(value))
72
+ return value.slice(0, 4) + '***';
73
+ return value;
74
+ }
75
+ /**
76
+ * Check if an IP address is private (RFC1918 or RFC6598 CGNAT)
77
+ */
78
+ export function isPrivateIP(ip) {
79
+ // Handle special values
80
+ if (ip === 'loopback' || ip === '127.0.0.1' || ip === '::1')
81
+ return true;
82
+ if (ip === 'localhost')
83
+ return true;
84
+ // Parse IPv4
85
+ const parts = ip.split('.');
86
+ if (parts.length !== 4)
87
+ return false;
88
+ const nums = parts.map(p => parseInt(p, 10));
89
+ if (nums.some(n => isNaN(n) || n < 0 || n > 255))
90
+ return false;
91
+ const [a, b] = nums;
92
+ // RFC1918: 10.0.0.0/8
93
+ if (a === 10)
94
+ return true;
95
+ // RFC1918: 172.16.0.0/12
96
+ if (a === 172 && b >= 16 && b <= 31)
97
+ return true;
98
+ // RFC1918: 192.168.0.0/16
99
+ if (a === 192 && b === 168)
100
+ return true;
101
+ // RFC6598 CGNAT: 100.64.0.0/10
102
+ if (a === 100 && b >= 64 && b <= 127)
103
+ return true;
104
+ // Loopback: 127.0.0.0/8
105
+ if (a === 127)
106
+ return true;
107
+ return false;
108
+ }
109
+ /**
110
+ * Check if gateway binding requires authentication
111
+ * Returns true if auth is configured
112
+ */
113
+ export function hasGatewayAuth(config) {
114
+ const token = getValueAtPath(config, 'gateway.auth.token');
115
+ const password = getValueAtPath(config, 'gateway.auth.password');
116
+ return (typeof token === 'string' && token.length > 0) ||
117
+ (typeof password === 'string' && password.length > 0);
118
+ }
119
+ /**
120
+ * API key patterns to detect
121
+ */
122
+ export const API_KEY_PATTERNS = [
123
+ { key: 'ANTHROPIC_API_KEY', pattern: /^sk-ant-/, name: 'Anthropic API key' },
124
+ { key: 'OPENAI_API_KEY', pattern: /^sk-/, name: 'OpenAI API key' },
125
+ { key: 'OPENAI_ORG_ID', pattern: /^org-/, name: 'OpenAI Organization ID' },
126
+ { key: 'COHERE_API_KEY', pattern: /^[A-Za-z0-9]{40}$/, name: 'Cohere API key' },
127
+ { key: 'GOOGLE_AI_API_KEY', pattern: /^AIza/, name: 'Google AI API key' },
128
+ { key: 'MISTRAL_API_KEY', pattern: /^[A-Za-z0-9]{32}$/, name: 'Mistral API key' },
129
+ { key: 'HUGGINGFACE_API_KEY', pattern: /^hf_/, name: 'HuggingFace API key' },
130
+ { key: 'REPLICATE_API_TOKEN', pattern: /^r8_/, name: 'Replicate API token' },
131
+ { key: 'AI21_API_KEY', pattern: /^[A-Za-z0-9]{40,}$/, name: 'AI21 API key' },
132
+ ];
133
+ /**
134
+ * Find hardcoded API keys in config
135
+ */
136
+ export function findHardcodedApiKeys(config) {
137
+ const found = [];
138
+ for (const { key, pattern, name } of API_KEY_PATTERNS) {
139
+ const value = config[key];
140
+ if (typeof value === 'string' && pattern.test(value)) {
141
+ found.push({ key, value, name });
142
+ }
143
+ }
144
+ return found;
145
+ }
146
+ //# sourceMappingURL=utils.js.map
package/package.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "@token-security/clawdit",
3
+ "version": "0.1.0",
4
+ "description": "Security audit tool for OpenClaw configurations",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "git+https://github.com/token-security/clawdit.git"
8
+ },
9
+ "type": "module",
10
+ "main": "dist/index.js",
11
+ "bin": {
12
+ "clawdit": "dist/index.js"
13
+ },
14
+ "scripts": {
15
+ "build": "tsc && chmod +x dist/index.js && cp src/schema/output.schema.json dist/clawdit-output.schema.json",
16
+ "start": "node dist/index.js",
17
+ "dev": "tsc --watch",
18
+ "test": "node --test tests/*.test.mjs dist/checks/*.test.js"
19
+ },
20
+ "files": [
21
+ "dist/**/*.js",
22
+ "dist/**/*.d.ts",
23
+ "dist/**/*.json",
24
+ "!dist/**/*.test.*",
25
+ "!dist/**/_template.*",
26
+ "!dist/**/*.map"
27
+ ],
28
+ "keywords": [
29
+ "openclaw",
30
+ "security",
31
+ "audit",
32
+ "cli"
33
+ ],
34
+ "author": "Token Security (Ido Shlomo)",
35
+ "license": "MIT",
36
+ "engines": {
37
+ "node": ">=18"
38
+ },
39
+ "dependencies": {
40
+ "json5": "^2.2.3"
41
+ },
42
+ "devDependencies": {
43
+ "@types/node": "^22.0.0",
44
+ "ajv": "^8.17.1",
45
+ "ajv-formats": "^3.0.1",
46
+ "typescript": "^5.7.0"
47
+ }
48
+ }