@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,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SEC-007: No custom redact patterns
|
|
3
|
+
*
|
|
4
|
+
* Detects when logging.redactPatterns is empty or not configured,
|
|
5
|
+
* relying only on built-in redaction patterns.
|
|
6
|
+
*/
|
|
7
|
+
import { getValueAtPath } from '../utils.js';
|
|
8
|
+
const check = {
|
|
9
|
+
id: 'SEC-007',
|
|
10
|
+
severity: 'LOW',
|
|
11
|
+
name: 'No custom redact patterns',
|
|
12
|
+
execute(ctx) {
|
|
13
|
+
const redactPatterns = getValueAtPath(ctx.config, 'logging.redactPatterns');
|
|
14
|
+
const redactSensitive = getValueAtPath(ctx.config, 'logging.redactSensitive');
|
|
15
|
+
// Only flag if redaction is enabled but no custom patterns
|
|
16
|
+
if (redactSensitive === 'off' || redactSensitive === false)
|
|
17
|
+
return [];
|
|
18
|
+
const hasPatterns = Array.isArray(redactPatterns) && redactPatterns.length > 0;
|
|
19
|
+
if (!hasPatterns) {
|
|
20
|
+
return [{
|
|
21
|
+
id: 'SEC-007',
|
|
22
|
+
severity: 'LOW',
|
|
23
|
+
name: 'No custom redact patterns',
|
|
24
|
+
location: { file: ctx.configPath, path: 'logging.redactPatterns' },
|
|
25
|
+
currentValue: redactPatterns ?? 'not set',
|
|
26
|
+
expectedValue: 'Array of regex patterns for sensitive data',
|
|
27
|
+
risk: 'Only built-in redaction patterns are active. Organization-specific sensitive data patterns (internal project names, custom tokens) may not be redacted.',
|
|
28
|
+
fix: {
|
|
29
|
+
description: 'Add custom redaction patterns for organization-specific sensitive data',
|
|
30
|
+
command: `jq '.logging.redactPatterns = ["(?i)internal-project-.*", "(?i)secret-.*"]' ${ctx.configPath} > tmp.json && mv tmp.json ${ctx.configPath}`,
|
|
31
|
+
},
|
|
32
|
+
references: ['https://docs.openclaw.ai/logging/security#custom-redaction'],
|
|
33
|
+
}];
|
|
34
|
+
}
|
|
35
|
+
return [];
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
export default check;
|
|
39
|
+
//# sourceMappingURL=sec-007-no-redact-patterns.js.map
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SEC-008: State directory has insecure permissions
|
|
3
|
+
*
|
|
4
|
+
* Detects when the OpenClaw state directory (~/.openclaw) exists with
|
|
5
|
+
* permissions that allow other users to read it.
|
|
6
|
+
*/
|
|
7
|
+
import type { Check } from './types.js';
|
|
8
|
+
declare const check: Check;
|
|
9
|
+
export default check;
|
|
10
|
+
//# sourceMappingURL=sec-008-state-dir-permissions.d.ts.map
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SEC-008: State directory has insecure permissions
|
|
3
|
+
*
|
|
4
|
+
* Detects when the OpenClaw state directory (~/.openclaw) exists with
|
|
5
|
+
* permissions that allow other users to read it.
|
|
6
|
+
*/
|
|
7
|
+
import { existsSync } from 'node:fs';
|
|
8
|
+
import { homedir } from 'node:os';
|
|
9
|
+
import { join } from 'node:path';
|
|
10
|
+
import { getFileMode, formatMode } from '../utils.js';
|
|
11
|
+
const check = {
|
|
12
|
+
id: 'SEC-008',
|
|
13
|
+
severity: 'MEDIUM',
|
|
14
|
+
name: 'State directory has insecure permissions',
|
|
15
|
+
execute(ctx) {
|
|
16
|
+
const stateDir = join(homedir(), '.openclaw');
|
|
17
|
+
// State directory doesn't exist yet - nothing to check
|
|
18
|
+
if (!existsSync(stateDir))
|
|
19
|
+
return [];
|
|
20
|
+
const mode = getFileMode(stateDir);
|
|
21
|
+
// Skip on Windows or if we can't read permissions
|
|
22
|
+
if (mode === null)
|
|
23
|
+
return [];
|
|
24
|
+
// Check if group or other has any permissions
|
|
25
|
+
const groupOther = mode & 0o077;
|
|
26
|
+
if (groupOther !== 0) {
|
|
27
|
+
return [{
|
|
28
|
+
id: 'SEC-008',
|
|
29
|
+
severity: 'MEDIUM',
|
|
30
|
+
name: 'State directory has insecure permissions',
|
|
31
|
+
location: {
|
|
32
|
+
file: stateDir,
|
|
33
|
+
path: null,
|
|
34
|
+
},
|
|
35
|
+
currentValue: formatMode(mode),
|
|
36
|
+
expectedValue: '700',
|
|
37
|
+
risk: 'The state directory may contain sensitive session data, tokens, or transcripts. Group/world-readable permissions expose this data to other users on the system.',
|
|
38
|
+
fix: {
|
|
39
|
+
description: 'Restrict directory permissions',
|
|
40
|
+
command: `chmod 700 ${stateDir}`,
|
|
41
|
+
},
|
|
42
|
+
references: [],
|
|
43
|
+
}];
|
|
44
|
+
}
|
|
45
|
+
return [];
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
export default check;
|
|
49
|
+
//# sourceMappingURL=sec-008-state-dir-permissions.js.map
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
export type Severity = 'HIGH' | 'MEDIUM' | 'LOW';
|
|
2
|
+
export interface Location {
|
|
3
|
+
file: string;
|
|
4
|
+
path: string | null;
|
|
5
|
+
}
|
|
6
|
+
export interface Fix {
|
|
7
|
+
description: string;
|
|
8
|
+
command: string;
|
|
9
|
+
}
|
|
10
|
+
export interface Finding {
|
|
11
|
+
id: string;
|
|
12
|
+
severity: Severity;
|
|
13
|
+
name: string;
|
|
14
|
+
location: Location;
|
|
15
|
+
currentValue: unknown;
|
|
16
|
+
expectedValue: unknown;
|
|
17
|
+
risk: string;
|
|
18
|
+
fix: Fix;
|
|
19
|
+
references: string[];
|
|
20
|
+
}
|
|
21
|
+
export interface CheckContext {
|
|
22
|
+
config: Record<string, unknown>;
|
|
23
|
+
configPath: string;
|
|
24
|
+
configDir: string;
|
|
25
|
+
}
|
|
26
|
+
export interface Check {
|
|
27
|
+
id: string;
|
|
28
|
+
severity: Severity;
|
|
29
|
+
name: string;
|
|
30
|
+
execute(ctx: CheckContext): Finding[];
|
|
31
|
+
}
|
|
32
|
+
export interface Summary {
|
|
33
|
+
total: number;
|
|
34
|
+
high: number;
|
|
35
|
+
medium: number;
|
|
36
|
+
low: number;
|
|
37
|
+
passed: number;
|
|
38
|
+
result: 'PASS' | 'FAIL';
|
|
39
|
+
}
|
|
40
|
+
export interface AuditResult {
|
|
41
|
+
findings: Finding[];
|
|
42
|
+
passed: string[];
|
|
43
|
+
summary: Summary;
|
|
44
|
+
}
|
|
45
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
|
+
"$id": "https://clawdit.dev/schemas/output.schema.json",
|
|
4
|
+
"title": "clawdit Output",
|
|
5
|
+
"description": "JSON output format for clawdit security audit tool",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"required": ["version", "timestamp", "config_path", "summary", "findings", "checks_passed"],
|
|
8
|
+
"additionalProperties": false,
|
|
9
|
+
"properties": {
|
|
10
|
+
"version": {
|
|
11
|
+
"type": "string",
|
|
12
|
+
"pattern": "^\\d+\\.\\d+\\.\\d+$",
|
|
13
|
+
"description": "clawdit version that produced this output"
|
|
14
|
+
},
|
|
15
|
+
"timestamp": {
|
|
16
|
+
"type": "string",
|
|
17
|
+
"format": "date-time",
|
|
18
|
+
"description": "ISO 8601 timestamp when the audit was run"
|
|
19
|
+
},
|
|
20
|
+
"config_path": {
|
|
21
|
+
"type": "string",
|
|
22
|
+
"description": "Path to the configuration file that was audited"
|
|
23
|
+
},
|
|
24
|
+
"summary": {
|
|
25
|
+
"$ref": "#/definitions/Summary"
|
|
26
|
+
},
|
|
27
|
+
"findings": {
|
|
28
|
+
"type": "array",
|
|
29
|
+
"items": {
|
|
30
|
+
"$ref": "#/definitions/Finding"
|
|
31
|
+
},
|
|
32
|
+
"description": "List of security findings (empty if all checks passed)"
|
|
33
|
+
},
|
|
34
|
+
"checks_passed": {
|
|
35
|
+
"type": "array",
|
|
36
|
+
"items": {
|
|
37
|
+
"$ref": "#/definitions/CheckId"
|
|
38
|
+
},
|
|
39
|
+
"description": "List of check IDs that passed"
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
"definitions": {
|
|
43
|
+
"Severity": {
|
|
44
|
+
"type": "string",
|
|
45
|
+
"enum": ["HIGH", "MEDIUM", "LOW"],
|
|
46
|
+
"description": "Severity level of a finding"
|
|
47
|
+
},
|
|
48
|
+
"CheckId": {
|
|
49
|
+
"type": "string",
|
|
50
|
+
"pattern": "^[A-Z]{2,5}-\\d{3}$",
|
|
51
|
+
"description": "Check identifier in format CAT-NNN (e.g., NET-001, AUTH-002)"
|
|
52
|
+
},
|
|
53
|
+
"Summary": {
|
|
54
|
+
"type": "object",
|
|
55
|
+
"required": ["total", "high", "medium", "low", "passed", "result"],
|
|
56
|
+
"additionalProperties": false,
|
|
57
|
+
"properties": {
|
|
58
|
+
"total": {
|
|
59
|
+
"type": "integer",
|
|
60
|
+
"minimum": 0,
|
|
61
|
+
"description": "Total number of findings"
|
|
62
|
+
},
|
|
63
|
+
"high": {
|
|
64
|
+
"type": "integer",
|
|
65
|
+
"minimum": 0,
|
|
66
|
+
"description": "Number of HIGH severity findings"
|
|
67
|
+
},
|
|
68
|
+
"medium": {
|
|
69
|
+
"type": "integer",
|
|
70
|
+
"minimum": 0,
|
|
71
|
+
"description": "Number of MEDIUM severity findings"
|
|
72
|
+
},
|
|
73
|
+
"low": {
|
|
74
|
+
"type": "integer",
|
|
75
|
+
"minimum": 0,
|
|
76
|
+
"description": "Number of LOW severity findings"
|
|
77
|
+
},
|
|
78
|
+
"passed": {
|
|
79
|
+
"type": "integer",
|
|
80
|
+
"minimum": 0,
|
|
81
|
+
"description": "Number of checks that passed"
|
|
82
|
+
},
|
|
83
|
+
"result": {
|
|
84
|
+
"type": "string",
|
|
85
|
+
"enum": ["PASS", "FAIL"],
|
|
86
|
+
"description": "Overall audit result"
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
},
|
|
90
|
+
"Location": {
|
|
91
|
+
"type": "object",
|
|
92
|
+
"required": ["file", "path"],
|
|
93
|
+
"additionalProperties": false,
|
|
94
|
+
"properties": {
|
|
95
|
+
"file": {
|
|
96
|
+
"type": "string",
|
|
97
|
+
"description": "Path to the configuration file"
|
|
98
|
+
},
|
|
99
|
+
"path": {
|
|
100
|
+
"type": ["string", "null"],
|
|
101
|
+
"description": "JSON path to the problematic value (null if file-level issue)"
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
},
|
|
105
|
+
"Fix": {
|
|
106
|
+
"type": "object",
|
|
107
|
+
"required": ["description", "command"],
|
|
108
|
+
"additionalProperties": false,
|
|
109
|
+
"properties": {
|
|
110
|
+
"description": {
|
|
111
|
+
"type": "string",
|
|
112
|
+
"description": "Human-readable description of the fix"
|
|
113
|
+
},
|
|
114
|
+
"command": {
|
|
115
|
+
"type": "string",
|
|
116
|
+
"description": "Command or code snippet to apply the fix"
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
},
|
|
120
|
+
"Finding": {
|
|
121
|
+
"type": "object",
|
|
122
|
+
"required": ["id", "severity", "name", "location", "current_value", "expected_value", "risk", "fix", "references"],
|
|
123
|
+
"additionalProperties": false,
|
|
124
|
+
"properties": {
|
|
125
|
+
"id": {
|
|
126
|
+
"$ref": "#/definitions/CheckId"
|
|
127
|
+
},
|
|
128
|
+
"severity": {
|
|
129
|
+
"$ref": "#/definitions/Severity"
|
|
130
|
+
},
|
|
131
|
+
"name": {
|
|
132
|
+
"type": "string",
|
|
133
|
+
"description": "Short description of the security issue"
|
|
134
|
+
},
|
|
135
|
+
"location": {
|
|
136
|
+
"$ref": "#/definitions/Location"
|
|
137
|
+
},
|
|
138
|
+
"current_value": {
|
|
139
|
+
"description": "The current (problematic) value found in the config"
|
|
140
|
+
},
|
|
141
|
+
"expected_value": {
|
|
142
|
+
"description": "The expected (secure) value"
|
|
143
|
+
},
|
|
144
|
+
"risk": {
|
|
145
|
+
"type": "string",
|
|
146
|
+
"description": "Explanation of the security risk"
|
|
147
|
+
},
|
|
148
|
+
"fix": {
|
|
149
|
+
"$ref": "#/definitions/Fix"
|
|
150
|
+
},
|
|
151
|
+
"references": {
|
|
152
|
+
"type": "array",
|
|
153
|
+
"items": {
|
|
154
|
+
"type": "string",
|
|
155
|
+
"format": "uri"
|
|
156
|
+
},
|
|
157
|
+
"description": "URLs to relevant documentation or security resources"
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { Check, Severity } from './checks/types.js';
|
|
2
|
+
export type OutputFormat = 'text' | 'json' | 'sarif';
|
|
3
|
+
export interface CliOptions {
|
|
4
|
+
configPath?: string;
|
|
5
|
+
outputFile?: string;
|
|
6
|
+
severity: Severity;
|
|
7
|
+
format: OutputFormat;
|
|
8
|
+
noColor: boolean;
|
|
9
|
+
quiet: boolean;
|
|
10
|
+
verbose: boolean;
|
|
11
|
+
version: boolean;
|
|
12
|
+
help: boolean;
|
|
13
|
+
listChecks: boolean;
|
|
14
|
+
checks?: string[];
|
|
15
|
+
skipChecks?: string[];
|
|
16
|
+
}
|
|
17
|
+
export declare function parseCliArgs(args: string[]): CliOptions;
|
|
18
|
+
export declare function validateCheckIds(ids: string[], validIds: string[]): string[];
|
|
19
|
+
export declare function printHelp(): void;
|
|
20
|
+
export declare function printVersion(): void;
|
|
21
|
+
export declare function printListChecksText(checks: Check[], noColor: boolean): void;
|
|
22
|
+
export declare function printListChecksJson(checks: Check[]): void;
|
|
23
|
+
//# sourceMappingURL=cli.d.ts.map
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { parseArgs } from 'node:util';
|
|
2
|
+
const HELP_TEXT = `
|
|
3
|
+
clawdit - OpenClaw Security Audit Tool
|
|
4
|
+
|
|
5
|
+
Usage: clawdit [OPTIONS] [CONFIG_PATH]
|
|
6
|
+
|
|
7
|
+
ARGUMENTS:
|
|
8
|
+
CONFIG_PATH Path to moltbot.json, or - to read from stdin (default: auto-discovered)
|
|
9
|
+
|
|
10
|
+
OPTIONS:
|
|
11
|
+
-o, --output <FILE> Write output to file instead of stdout
|
|
12
|
+
-s, --severity <LEVEL> Minimum severity to report: low, medium, high (default: low)
|
|
13
|
+
-f, --format <FORMAT> Output format: text, json, sarif (default: text if TTY, json otherwise)
|
|
14
|
+
--checks <IDS> Run only specified checks (comma-separated)
|
|
15
|
+
--skip-checks <IDS> Skip specified checks (comma-separated)
|
|
16
|
+
--no-color Disable colored output
|
|
17
|
+
-q, --quiet Suppress banner and summary; output findings only
|
|
18
|
+
-v, --verbose Show check execution progress
|
|
19
|
+
--list-checks List all available security checks and exit
|
|
20
|
+
--version Print version and exit
|
|
21
|
+
--help Show this help message
|
|
22
|
+
|
|
23
|
+
EXAMPLES:
|
|
24
|
+
clawdit # Auto-discover config, audit with text output
|
|
25
|
+
clawdit ~/.clawdbot/moltbot.json # Audit specific config
|
|
26
|
+
cat config.json | clawdit - # Read config from stdin
|
|
27
|
+
clawdit --severity high # Only report HIGH severity findings
|
|
28
|
+
clawdit -o audit.txt # Write output to file
|
|
29
|
+
|
|
30
|
+
EXIT CODES:
|
|
31
|
+
0 All checks passed
|
|
32
|
+
1 HIGH severity findings
|
|
33
|
+
2 MEDIUM severity findings (no HIGH)
|
|
34
|
+
3 LOW severity findings only
|
|
35
|
+
10 Configuration file not found
|
|
36
|
+
11 Configuration parse error
|
|
37
|
+
12 Invalid CLI arguments
|
|
38
|
+
13 Permission denied
|
|
39
|
+
14 $include directive failed
|
|
40
|
+
20 Internal error
|
|
41
|
+
`;
|
|
42
|
+
export function parseCliArgs(args) {
|
|
43
|
+
try {
|
|
44
|
+
const { values, positionals } = parseArgs({
|
|
45
|
+
args,
|
|
46
|
+
options: {
|
|
47
|
+
output: { type: 'string', short: 'o' },
|
|
48
|
+
severity: { type: 'string', short: 's', default: 'low' },
|
|
49
|
+
format: { type: 'string', short: 'f' },
|
|
50
|
+
checks: { type: 'string' },
|
|
51
|
+
'skip-checks': { type: 'string' },
|
|
52
|
+
'no-color': { type: 'boolean', default: false },
|
|
53
|
+
quiet: { type: 'boolean', short: 'q', default: false },
|
|
54
|
+
verbose: { type: 'boolean', short: 'v', default: false },
|
|
55
|
+
'list-checks': { type: 'boolean', default: false },
|
|
56
|
+
version: { type: 'boolean', default: false },
|
|
57
|
+
help: { type: 'boolean', default: false },
|
|
58
|
+
},
|
|
59
|
+
allowPositionals: true,
|
|
60
|
+
});
|
|
61
|
+
// Validate severity
|
|
62
|
+
const severityMap = {
|
|
63
|
+
low: 'LOW',
|
|
64
|
+
medium: 'MEDIUM',
|
|
65
|
+
high: 'HIGH',
|
|
66
|
+
};
|
|
67
|
+
const severityInput = values.severity.toLowerCase();
|
|
68
|
+
if (!severityMap[severityInput]) {
|
|
69
|
+
throw new Error(`Invalid severity level: ${values.severity}. Must be: low, medium, high`);
|
|
70
|
+
}
|
|
71
|
+
// Validate format (default to text for TTY, json otherwise)
|
|
72
|
+
const formatInput = values.format
|
|
73
|
+
? values.format.toLowerCase()
|
|
74
|
+
: (process.stdout.isTTY ? 'text' : 'json');
|
|
75
|
+
if (formatInput !== 'text' && formatInput !== 'json' && formatInput !== 'sarif') {
|
|
76
|
+
throw new Error(`Invalid format: ${values.format}. Must be: text, json, sarif`);
|
|
77
|
+
}
|
|
78
|
+
return {
|
|
79
|
+
configPath: positionals[0],
|
|
80
|
+
outputFile: values.output,
|
|
81
|
+
severity: severityMap[severityInput],
|
|
82
|
+
format: formatInput,
|
|
83
|
+
noColor: values['no-color'] || !process.stdout.isTTY,
|
|
84
|
+
quiet: values.quiet,
|
|
85
|
+
verbose: values.verbose,
|
|
86
|
+
version: values.version,
|
|
87
|
+
help: values.help,
|
|
88
|
+
listChecks: values['list-checks'],
|
|
89
|
+
checks: values.checks
|
|
90
|
+
? values.checks.split(',').map(s => s.trim().toUpperCase())
|
|
91
|
+
: undefined,
|
|
92
|
+
skipChecks: values['skip-checks']
|
|
93
|
+
? values['skip-checks'].split(',').map(s => s.trim().toUpperCase())
|
|
94
|
+
: undefined,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
catch (error) {
|
|
98
|
+
const err = error;
|
|
99
|
+
console.error(`Error: ${err.message}`);
|
|
100
|
+
console.error('Use --help for usage information');
|
|
101
|
+
process.exit(12);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
export function validateCheckIds(ids, validIds) {
|
|
105
|
+
const validSet = new Set(validIds);
|
|
106
|
+
return ids.filter(id => !validSet.has(id));
|
|
107
|
+
}
|
|
108
|
+
export function printHelp() {
|
|
109
|
+
console.log(HELP_TEXT.trim());
|
|
110
|
+
}
|
|
111
|
+
export function printVersion() {
|
|
112
|
+
console.log('clawdit v0.1.0');
|
|
113
|
+
}
|
|
114
|
+
export function printListChecksText(checks, noColor) {
|
|
115
|
+
const c = noColor
|
|
116
|
+
? { reset: '', bold: '', dim: '', red: '', yellow: '', cyan: '' }
|
|
117
|
+
: { reset: '\x1b[0m', bold: '\x1b[1m', dim: '\x1b[2m', red: '\x1b[31m', yellow: '\x1b[33m', cyan: '\x1b[36m' };
|
|
118
|
+
const severityColor = (s) => s === 'HIGH' ? c.red : s === 'MEDIUM' ? c.yellow : c.cyan;
|
|
119
|
+
console.log(`${c.bold}ID SEVERITY NAME${c.reset}`);
|
|
120
|
+
for (const check of checks) {
|
|
121
|
+
const sev = severityColor(check.severity) + check.severity.padEnd(8) + c.reset;
|
|
122
|
+
console.log(`${check.id.padEnd(12)}${sev} ${check.name}`);
|
|
123
|
+
}
|
|
124
|
+
console.log(`${c.dim}${checks.length} checks available${c.reset}`);
|
|
125
|
+
}
|
|
126
|
+
export function printListChecksJson(checks) {
|
|
127
|
+
const output = {
|
|
128
|
+
checks: checks.map(c => ({ id: c.id, severity: c.severity, name: c.name }))
|
|
129
|
+
};
|
|
130
|
+
console.log(JSON.stringify(output, null, 2));
|
|
131
|
+
}
|
|
132
|
+
//# sourceMappingURL=cli.js.map
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Read content from stdin synchronously
|
|
3
|
+
*/
|
|
4
|
+
export declare function readStdinSync(): string;
|
|
5
|
+
export declare class ConfigError extends Error {
|
|
6
|
+
code: number;
|
|
7
|
+
details?: string | undefined;
|
|
8
|
+
constructor(message: string, code: number, details?: string | undefined);
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Discover config file path in priority order
|
|
12
|
+
*/
|
|
13
|
+
export declare function discoverConfig(cliPath?: string): string;
|
|
14
|
+
/**
|
|
15
|
+
* Parse config from string content with $include resolution
|
|
16
|
+
*/
|
|
17
|
+
export declare function parseConfigFromString(content: string, baseDir: string, sourceName?: string): Record<string, unknown>;
|
|
18
|
+
/**
|
|
19
|
+
* Parse config file with $include resolution
|
|
20
|
+
*/
|
|
21
|
+
export declare function parseConfig(configPath: string): Record<string, unknown>;
|
|
22
|
+
//# sourceMappingURL=config.d.ts.map
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { readFileSync, existsSync } from 'node:fs';
|
|
2
|
+
import { resolve, dirname, join } from 'node:path';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
import JSON5 from 'json5';
|
|
5
|
+
/**
|
|
6
|
+
* Read content from stdin synchronously
|
|
7
|
+
*/
|
|
8
|
+
export function readStdinSync() {
|
|
9
|
+
try {
|
|
10
|
+
return readFileSync(0, 'utf-8');
|
|
11
|
+
}
|
|
12
|
+
catch (error) {
|
|
13
|
+
throw new ConfigError('Failed to read from stdin', 20, error.message);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
export class ConfigError extends Error {
|
|
17
|
+
code;
|
|
18
|
+
details;
|
|
19
|
+
constructor(message, code, details) {
|
|
20
|
+
super(message);
|
|
21
|
+
this.code = code;
|
|
22
|
+
this.details = details;
|
|
23
|
+
this.name = 'ConfigError';
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Discover config file path in priority order
|
|
28
|
+
*/
|
|
29
|
+
export function discoverConfig(cliPath) {
|
|
30
|
+
// 1. CLI argument (highest priority)
|
|
31
|
+
if (cliPath) {
|
|
32
|
+
const resolved = resolve(cliPath);
|
|
33
|
+
if (!existsSync(resolved)) {
|
|
34
|
+
throw new ConfigError('Configuration file not found', 10, `Path: ${resolved}\n\nPossible causes:\n - OpenClaw is not installed\n - Config is in a non-standard location\n - Running as wrong user`);
|
|
35
|
+
}
|
|
36
|
+
return resolved;
|
|
37
|
+
}
|
|
38
|
+
// 2. CLAWDBOT_CONFIG environment variable
|
|
39
|
+
const envPath = process.env.CLAWDBOT_CONFIG;
|
|
40
|
+
if (envPath) {
|
|
41
|
+
const resolved = resolve(envPath);
|
|
42
|
+
if (existsSync(resolved))
|
|
43
|
+
return resolved;
|
|
44
|
+
}
|
|
45
|
+
// 3. Search paths in priority order
|
|
46
|
+
const home = homedir();
|
|
47
|
+
const CONFIG_FILENAMES = ['openclaw.json', 'moltbot.json', 'clawdbot.json'];
|
|
48
|
+
const CONFIG_DIRS = [
|
|
49
|
+
'.',
|
|
50
|
+
join(home, '.openclaw'),
|
|
51
|
+
join(home, '.clawdbot'),
|
|
52
|
+
join(home, '.moltbot'),
|
|
53
|
+
];
|
|
54
|
+
// Generate all combinations, prioritizing openclaw.json in each directory
|
|
55
|
+
const searchPaths = CONFIG_DIRS.flatMap(dir => CONFIG_FILENAMES.map(file => join(dir, file)));
|
|
56
|
+
for (const searchPath of searchPaths) {
|
|
57
|
+
const resolved = resolve(searchPath);
|
|
58
|
+
if (existsSync(resolved))
|
|
59
|
+
return resolved;
|
|
60
|
+
}
|
|
61
|
+
throw new ConfigError('Configuration file not found', 10, `Searched paths:\n${searchPaths.map(p => ` - ${p}`).join('\n')}\n\nActions:\n 1. Install OpenClaw: curl -fsSL https://get.openclaw.ai | sh\n 2. Or specify path: clawdit /path/to/openclaw.json`);
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Parse config from string content with $include resolution
|
|
65
|
+
*/
|
|
66
|
+
export function parseConfigFromString(content, baseDir, sourceName = '<stdin>') {
|
|
67
|
+
let config;
|
|
68
|
+
try {
|
|
69
|
+
config = JSON5.parse(content);
|
|
70
|
+
}
|
|
71
|
+
catch (error) {
|
|
72
|
+
throw new ConfigError(`JSON5 parse error`, 11, `Source: ${sourceName}\nError: ${error.message}`);
|
|
73
|
+
}
|
|
74
|
+
// Process $include directives with empty inclusion chain
|
|
75
|
+
return processIncludes(config, baseDir, new Set());
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Parse config file with $include resolution
|
|
79
|
+
*/
|
|
80
|
+
export function parseConfig(configPath) {
|
|
81
|
+
const inclusionChain = new Set();
|
|
82
|
+
return parseConfigRecursive(configPath, inclusionChain);
|
|
83
|
+
}
|
|
84
|
+
function parseConfigRecursive(configPath, inclusionChain) {
|
|
85
|
+
const resolvedPath = resolve(configPath);
|
|
86
|
+
// Check for circular inclusion
|
|
87
|
+
if (inclusionChain.has(resolvedPath)) {
|
|
88
|
+
throw new ConfigError('$include directive failed: circular reference detected', 14, `Circular inclusion chain:\n${[...inclusionChain, resolvedPath].map(p => ` -> ${p}`).join('\n')}`);
|
|
89
|
+
}
|
|
90
|
+
inclusionChain.add(resolvedPath);
|
|
91
|
+
// Read and parse file
|
|
92
|
+
let content;
|
|
93
|
+
try {
|
|
94
|
+
content = readFileSync(resolvedPath, 'utf-8');
|
|
95
|
+
}
|
|
96
|
+
catch (err) {
|
|
97
|
+
const error = err;
|
|
98
|
+
if (error.code === 'ENOENT') {
|
|
99
|
+
throw new ConfigError('Configuration file not found', 10, `Path: ${resolvedPath}`);
|
|
100
|
+
}
|
|
101
|
+
if (error.code === 'EACCES') {
|
|
102
|
+
throw new ConfigError('Permission denied reading configuration', 13, `Path: ${resolvedPath}\n\nCheck file permissions: ls -la ${resolvedPath}`);
|
|
103
|
+
}
|
|
104
|
+
throw new ConfigError('Failed to read configuration file', 20, `Path: ${resolvedPath}\nError: ${error.message}`);
|
|
105
|
+
}
|
|
106
|
+
let config;
|
|
107
|
+
try {
|
|
108
|
+
config = JSON5.parse(content);
|
|
109
|
+
}
|
|
110
|
+
catch (err) {
|
|
111
|
+
const error = err;
|
|
112
|
+
throw new ConfigError('JSON5 parse error', 11, `Path: ${resolvedPath}\nError: ${error.message}`);
|
|
113
|
+
}
|
|
114
|
+
// Process $include directives
|
|
115
|
+
const configDir = dirname(resolvedPath);
|
|
116
|
+
config = processIncludes(config, configDir, inclusionChain);
|
|
117
|
+
inclusionChain.delete(resolvedPath);
|
|
118
|
+
return config;
|
|
119
|
+
}
|
|
120
|
+
function processIncludes(config, baseDir, inclusionChain) {
|
|
121
|
+
const result = {};
|
|
122
|
+
for (const [key, value] of Object.entries(config)) {
|
|
123
|
+
if (key === '$include') {
|
|
124
|
+
// Handle $include directive
|
|
125
|
+
if (typeof value === 'string') {
|
|
126
|
+
const includePath = resolve(baseDir, value);
|
|
127
|
+
const included = parseConfigRecursive(includePath, new Set(inclusionChain));
|
|
128
|
+
Object.assign(result, included);
|
|
129
|
+
}
|
|
130
|
+
else if (Array.isArray(value)) {
|
|
131
|
+
for (const item of value) {
|
|
132
|
+
if (typeof item === 'string') {
|
|
133
|
+
const includePath = resolve(baseDir, item);
|
|
134
|
+
const included = parseConfigRecursive(includePath, new Set(inclusionChain));
|
|
135
|
+
Object.assign(result, included);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
else if (value !== null && typeof value === 'object' && !Array.isArray(value)) {
|
|
141
|
+
// Recursively process nested objects
|
|
142
|
+
result[key] = processIncludes(value, baseDir, inclusionChain);
|
|
143
|
+
}
|
|
144
|
+
else {
|
|
145
|
+
result[key] = value;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return result;
|
|
149
|
+
}
|
|
150
|
+
//# sourceMappingURL=config.js.map
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { AuditResult } from './checks/types.js';
|
|
2
|
+
interface FormatOptions {
|
|
3
|
+
noColor: boolean;
|
|
4
|
+
quiet: boolean;
|
|
5
|
+
configPath: string;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Result of formatText split into parts for proper stream separation
|
|
9
|
+
* - banner: Goes to stderr (chrome/metadata)
|
|
10
|
+
* - findings: Goes to stdout (data)
|
|
11
|
+
* - summary: Goes to stderr (chrome/metadata)
|
|
12
|
+
*/
|
|
13
|
+
export interface TextFormatResult {
|
|
14
|
+
banner: string;
|
|
15
|
+
findings: string;
|
|
16
|
+
summary: string;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Format audit result as text, split into parts for proper stream separation
|
|
20
|
+
*/
|
|
21
|
+
export declare function formatText(result: AuditResult, options: FormatOptions): TextFormatResult;
|
|
22
|
+
/**
|
|
23
|
+
* Format audit result as text, joined as a single string.
|
|
24
|
+
* Used for file output where stream separation is not needed.
|
|
25
|
+
*/
|
|
26
|
+
export declare function formatTextFull(result: AuditResult, options: FormatOptions): string;
|
|
27
|
+
interface JsonFormatOptions {
|
|
28
|
+
configPath: string;
|
|
29
|
+
}
|
|
30
|
+
interface SarifFormatOptions {
|
|
31
|
+
configPath: string;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Format audit result as SARIF 2.1.0
|
|
35
|
+
*/
|
|
36
|
+
export declare function formatSarif(result: AuditResult, options: SarifFormatOptions): string;
|
|
37
|
+
/**
|
|
38
|
+
* Format audit result as JSON
|
|
39
|
+
*/
|
|
40
|
+
export declare function formatJson(result: AuditResult, options: JsonFormatOptions): string;
|
|
41
|
+
export {};
|
|
42
|
+
//# sourceMappingURL=formatter.d.ts.map
|