@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.
- package/LICENSE +21 -0
- package/README.md +197 -0
- package/dist/checks/auth-001-device-auth-disabled.d.ts +10 -0
- package/dist/checks/auth-001-device-auth-disabled.js +34 -0
- package/dist/checks/auth-002-insecure-fallback.d.ts +10 -0
- package/dist/checks/auth-002-insecure-fallback.js +34 -0
- package/dist/checks/auth-003-no-gateway-auth.d.ts +10 -0
- package/dist/checks/auth-003-no-gateway-auth.js +40 -0
- package/dist/checks/auth-004-public-trusted-proxies.d.ts +10 -0
- package/dist/checks/auth-004-public-trusted-proxies.js +37 -0
- package/dist/checks/auth-005-hooks-no-token.d.ts +10 -0
- package/dist/checks/auth-005-hooks-no-token.js +42 -0
- package/dist/checks/auth-006-pairing-exposed.d.ts +10 -0
- package/dist/checks/auth-006-pairing-exposed.js +46 -0
- package/dist/checks/auth-007-missing-trusted-proxies.d.ts +11 -0
- package/dist/checks/auth-007-missing-trusted-proxies.js +46 -0
- package/dist/checks/chan-001-open-dm.d.ts +10 -0
- package/dist/checks/chan-001-open-dm.js +48 -0
- package/dist/checks/chan-002-group-policy.d.ts +10 -0
- package/dist/checks/chan-002-group-policy.js +43 -0
- package/dist/checks/chan-003-no-mention.d.ts +10 -0
- package/dist/checks/chan-003-no-mention.js +45 -0
- package/dist/checks/chan-004-dm-isolation.d.ts +10 -0
- package/dist/checks/chan-004-dm-isolation.js +50 -0
- package/dist/checks/chan-005-verbose-groups.d.ts +10 -0
- package/dist/checks/chan-005-verbose-groups.js +53 -0
- package/dist/checks/disc-001-mdns-full.d.ts +10 -0
- package/dist/checks/disc-001-mdns-full.js +34 -0
- package/dist/checks/disc-002-mdns-enabled.d.ts +10 -0
- package/dist/checks/disc-002-mdns-enabled.js +35 -0
- package/dist/checks/exec-001-full-security.d.ts +10 -0
- package/dist/checks/exec-001-full-security.js +34 -0
- package/dist/checks/exec-002-sandbox-disabled.d.ts +10 -0
- package/dist/checks/exec-002-sandbox-disabled.js +34 -0
- package/dist/checks/exec-003-elevated-unrestricted.d.ts +10 -0
- package/dist/checks/exec-003-elevated-unrestricted.js +38 -0
- package/dist/checks/exec-004-approval-fallback.d.ts +10 -0
- package/dist/checks/exec-004-approval-fallback.js +50 -0
- package/dist/checks/exec-005-sandbox-non-main.d.ts +10 -0
- package/dist/checks/exec-005-sandbox-non-main.js +34 -0
- package/dist/checks/exec-006-cross-agent-sandbox.d.ts +10 -0
- package/dist/checks/exec-006-cross-agent-sandbox.js +34 -0
- package/dist/checks/exec-007-workspace-rw.d.ts +10 -0
- package/dist/checks/exec-007-workspace-rw.js +34 -0
- package/dist/checks/index.d.ts +16 -0
- package/dist/checks/index.js +94 -0
- package/dist/checks/loader.d.ts +38 -0
- package/dist/checks/loader.js +149 -0
- package/dist/checks/model-001-weak-model-tools.d.ts +10 -0
- package/dist/checks/model-001-weak-model-tools.js +68 -0
- package/dist/checks/net-001-gateway-binding.d.ts +10 -0
- package/dist/checks/net-001-gateway-binding.js +34 -0
- package/dist/checks/net-002-default-port.d.ts +10 -0
- package/dist/checks/net-002-default-port.js +35 -0
- package/dist/checks/net-003-tailnet-no-token.d.ts +10 -0
- package/dist/checks/net-003-tailnet-no-token.js +34 -0
- package/dist/checks/plug-001-no-allowlist.d.ts +10 -0
- package/dist/checks/plug-001-no-allowlist.js +52 -0
- package/dist/checks/plug-002-extensions-exposed.d.ts +10 -0
- package/dist/checks/plug-002-extensions-exposed.js +41 -0
- package/dist/checks/runner.d.ts +14 -0
- package/dist/checks/runner.js +72 -0
- package/dist/checks/schema.d.ts +54 -0
- package/dist/checks/schema.js +171 -0
- package/dist/checks/sec-001-hardcoded-keys.d.ts +10 -0
- package/dist/checks/sec-001-hardcoded-keys.js +34 -0
- package/dist/checks/sec-002-world-readable-config.d.ts +10 -0
- package/dist/checks/sec-002-world-readable-config.js +39 -0
- package/dist/checks/sec-003-credentials-exposed.d.ts +10 -0
- package/dist/checks/sec-003-credentials-exposed.js +41 -0
- package/dist/checks/sec-004-env-readable.d.ts +10 -0
- package/dist/checks/sec-004-env-readable.js +40 -0
- package/dist/checks/sec-005-transcripts-exposed.d.ts +11 -0
- package/dist/checks/sec-005-transcripts-exposed.js +62 -0
- package/dist/checks/sec-006-redaction-disabled.d.ts +10 -0
- package/dist/checks/sec-006-redaction-disabled.js +34 -0
- package/dist/checks/sec-007-no-redact-patterns.d.ts +10 -0
- package/dist/checks/sec-007-no-redact-patterns.js +39 -0
- package/dist/checks/sec-008-state-dir-permissions.d.ts +10 -0
- package/dist/checks/sec-008-state-dir-permissions.js +49 -0
- package/dist/checks/types.d.ts +45 -0
- package/dist/checks/types.js +2 -0
- package/dist/clawdit-output.schema.json +162 -0
- package/dist/cli.d.ts +23 -0
- package/dist/cli.js +132 -0
- package/dist/config.d.ts +22 -0
- package/dist/config.js +150 -0
- package/dist/formatter.d.ts +42 -0
- package/dist/formatter.js +233 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +168 -0
- package/dist/utils.d.ts +46 -0
- package/dist/utils.js +146 -0
- 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
|
package/dist/index.d.ts
ADDED
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
|
package/dist/utils.d.ts
ADDED
|
@@ -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
|
+
}
|