@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,14 @@
|
|
|
1
|
+
import type { Check, CheckContext, AuditResult, Severity } from './types.js';
|
|
2
|
+
export interface RunChecksOptions {
|
|
3
|
+
minSeverity?: Severity;
|
|
4
|
+
onCheckComplete?: (checkId: string, passed: boolean) => void;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Run all checks and collect findings
|
|
8
|
+
*/
|
|
9
|
+
export declare function runChecks(checks: Check[], ctx: CheckContext, options?: RunChecksOptions): AuditResult;
|
|
10
|
+
/**
|
|
11
|
+
* Get exit code based on audit result
|
|
12
|
+
*/
|
|
13
|
+
export declare function getExitCode(result: AuditResult): number;
|
|
14
|
+
//# sourceMappingURL=runner.d.ts.map
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Run all checks and collect findings
|
|
3
|
+
*/
|
|
4
|
+
export function runChecks(checks, ctx, options = {}) {
|
|
5
|
+
const { minSeverity = 'LOW', onCheckComplete } = options;
|
|
6
|
+
const findings = [];
|
|
7
|
+
const passed = [];
|
|
8
|
+
const severityOrder = {
|
|
9
|
+
'HIGH': 3,
|
|
10
|
+
'MEDIUM': 2,
|
|
11
|
+
'LOW': 1,
|
|
12
|
+
};
|
|
13
|
+
const minSeverityLevel = severityOrder[minSeverity];
|
|
14
|
+
for (const check of checks) {
|
|
15
|
+
// Skip checks below minimum severity threshold
|
|
16
|
+
if (severityOrder[check.severity] < minSeverityLevel) {
|
|
17
|
+
continue;
|
|
18
|
+
}
|
|
19
|
+
try {
|
|
20
|
+
const results = check.execute(ctx);
|
|
21
|
+
if (results.length > 0) {
|
|
22
|
+
findings.push(...results);
|
|
23
|
+
if (onCheckComplete) {
|
|
24
|
+
onCheckComplete(check.id, false);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
else {
|
|
28
|
+
passed.push(check.id);
|
|
29
|
+
if (onCheckComplete) {
|
|
30
|
+
onCheckComplete(check.id, true);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
catch (error) {
|
|
35
|
+
// Check execution errors are treated as passed (fail-open for individual checks)
|
|
36
|
+
// This matches the spec: "missing config paths treated as passed"
|
|
37
|
+
passed.push(check.id);
|
|
38
|
+
if (onCheckComplete) {
|
|
39
|
+
onCheckComplete(check.id, true);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
const summary = computeSummary(findings, passed);
|
|
44
|
+
return { findings, passed, summary };
|
|
45
|
+
}
|
|
46
|
+
function computeSummary(findings, passed) {
|
|
47
|
+
const high = findings.filter(f => f.severity === 'HIGH').length;
|
|
48
|
+
const medium = findings.filter(f => f.severity === 'MEDIUM').length;
|
|
49
|
+
const low = findings.filter(f => f.severity === 'LOW').length;
|
|
50
|
+
const result = findings.length > 0 ? 'FAIL' : 'PASS';
|
|
51
|
+
return {
|
|
52
|
+
total: findings.length,
|
|
53
|
+
high,
|
|
54
|
+
medium,
|
|
55
|
+
low,
|
|
56
|
+
passed: passed.length,
|
|
57
|
+
result,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Get exit code based on audit result
|
|
62
|
+
*/
|
|
63
|
+
export function getExitCode(result) {
|
|
64
|
+
if (result.summary.high > 0)
|
|
65
|
+
return 1;
|
|
66
|
+
if (result.summary.medium > 0)
|
|
67
|
+
return 2;
|
|
68
|
+
if (result.summary.low > 0)
|
|
69
|
+
return 3;
|
|
70
|
+
return 0;
|
|
71
|
+
}
|
|
72
|
+
//# sourceMappingURL=runner.js.map
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Check validation schema
|
|
3
|
+
*
|
|
4
|
+
* Validates check definitions at load time to catch agent mistakes early.
|
|
5
|
+
* This ensures all checks follow the required pattern and naming conventions.
|
|
6
|
+
*/
|
|
7
|
+
import type { Check, Severity } from './types.js';
|
|
8
|
+
/**
|
|
9
|
+
* Valid check categories
|
|
10
|
+
*/
|
|
11
|
+
export declare const VALID_CATEGORIES: readonly ["NET", "AUTH", "EXEC", "SEC", "DISC", "CHAN", "MODEL", "PLUG"];
|
|
12
|
+
export type Category = typeof VALID_CATEGORIES[number];
|
|
13
|
+
/**
|
|
14
|
+
* Valid severity levels
|
|
15
|
+
*/
|
|
16
|
+
export declare const VALID_SEVERITIES: Severity[];
|
|
17
|
+
export interface ValidationError {
|
|
18
|
+
field: string;
|
|
19
|
+
message: string;
|
|
20
|
+
value?: unknown;
|
|
21
|
+
}
|
|
22
|
+
export interface ValidationResult {
|
|
23
|
+
valid: boolean;
|
|
24
|
+
errors: ValidationError[];
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Validate a check ID format
|
|
28
|
+
*/
|
|
29
|
+
export declare function validateCheckId(id: string): ValidationResult;
|
|
30
|
+
/**
|
|
31
|
+
* Validate that a filename matches the check ID
|
|
32
|
+
*/
|
|
33
|
+
export declare function validateFilename(filename: string, checkId: string): ValidationResult;
|
|
34
|
+
/**
|
|
35
|
+
* Validate a check object has all required fields
|
|
36
|
+
*/
|
|
37
|
+
export declare function validateCheck(check: unknown, filename?: string): ValidationResult;
|
|
38
|
+
/**
|
|
39
|
+
* Format validation errors for display
|
|
40
|
+
*/
|
|
41
|
+
export declare function formatValidationErrors(errors: ValidationError[], filename?: string): string;
|
|
42
|
+
/**
|
|
43
|
+
* Validate a check and throw if invalid
|
|
44
|
+
*/
|
|
45
|
+
export declare function assertValidCheck(check: unknown, filename?: string): asserts check is Check;
|
|
46
|
+
/**
|
|
47
|
+
* Extract category from check ID
|
|
48
|
+
*/
|
|
49
|
+
export declare function getCategoryFromId(id: string): Category | null;
|
|
50
|
+
/**
|
|
51
|
+
* Extract number from check ID (e.g., "001" from "NET-001")
|
|
52
|
+
*/
|
|
53
|
+
export declare function getNumberFromId(id: string): string | null;
|
|
54
|
+
//# sourceMappingURL=schema.d.ts.map
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Check validation schema
|
|
3
|
+
*
|
|
4
|
+
* Validates check definitions at load time to catch agent mistakes early.
|
|
5
|
+
* This ensures all checks follow the required pattern and naming conventions.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Valid check categories
|
|
9
|
+
*/
|
|
10
|
+
export const VALID_CATEGORIES = ['NET', 'AUTH', 'EXEC', 'SEC', 'DISC', 'CHAN', 'MODEL', 'PLUG'];
|
|
11
|
+
/**
|
|
12
|
+
* Valid severity levels
|
|
13
|
+
*/
|
|
14
|
+
export const VALID_SEVERITIES = ['HIGH', 'MEDIUM', 'LOW'];
|
|
15
|
+
/**
|
|
16
|
+
* Check ID pattern: CAT-NNN (e.g., NET-001, AUTH-002)
|
|
17
|
+
*/
|
|
18
|
+
const CHECK_ID_PATTERN = /^([A-Z]+)-(\d{3})$/;
|
|
19
|
+
/**
|
|
20
|
+
* Expected filename pattern: cat-nnn-description.ts (e.g., net-001-gateway-binding.ts)
|
|
21
|
+
*/
|
|
22
|
+
const FILENAME_PATTERN = /^([a-z]+)-(\d{3})-[a-z0-9-]+\.ts$/;
|
|
23
|
+
/**
|
|
24
|
+
* Validate a check ID format
|
|
25
|
+
*/
|
|
26
|
+
export function validateCheckId(id) {
|
|
27
|
+
const errors = [];
|
|
28
|
+
const match = id.match(CHECK_ID_PATTERN);
|
|
29
|
+
if (!match) {
|
|
30
|
+
errors.push({
|
|
31
|
+
field: 'id',
|
|
32
|
+
message: `Check ID must match pattern CAT-NNN (e.g., NET-001). Got: ${id}`,
|
|
33
|
+
value: id,
|
|
34
|
+
});
|
|
35
|
+
return { valid: false, errors };
|
|
36
|
+
}
|
|
37
|
+
const category = match[1];
|
|
38
|
+
if (!VALID_CATEGORIES.includes(category)) {
|
|
39
|
+
errors.push({
|
|
40
|
+
field: 'id',
|
|
41
|
+
message: `Invalid category "${category}". Valid categories: ${VALID_CATEGORIES.join(', ')}`,
|
|
42
|
+
value: category,
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
return { valid: errors.length === 0, errors };
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Validate that a filename matches the check ID
|
|
49
|
+
*/
|
|
50
|
+
export function validateFilename(filename, checkId) {
|
|
51
|
+
const errors = [];
|
|
52
|
+
const match = filename.match(FILENAME_PATTERN);
|
|
53
|
+
if (!match) {
|
|
54
|
+
errors.push({
|
|
55
|
+
field: 'filename',
|
|
56
|
+
message: `Filename must match pattern cat-nnn-description.ts (e.g., net-001-gateway-binding.ts). Got: ${filename}`,
|
|
57
|
+
value: filename,
|
|
58
|
+
});
|
|
59
|
+
return { valid: false, errors };
|
|
60
|
+
}
|
|
61
|
+
// Extract category and number from filename
|
|
62
|
+
const fileCategory = match[1].toUpperCase();
|
|
63
|
+
const fileNumber = match[2];
|
|
64
|
+
// Extract from check ID
|
|
65
|
+
const idMatch = checkId.match(CHECK_ID_PATTERN);
|
|
66
|
+
if (!idMatch) {
|
|
67
|
+
// ID validation will catch this
|
|
68
|
+
return { valid: true, errors };
|
|
69
|
+
}
|
|
70
|
+
const idCategory = idMatch[1];
|
|
71
|
+
const idNumber = idMatch[2];
|
|
72
|
+
if (fileCategory !== idCategory || fileNumber !== idNumber) {
|
|
73
|
+
errors.push({
|
|
74
|
+
field: 'filename',
|
|
75
|
+
message: `Filename "${filename}" does not match check ID "${checkId}". Expected: ${idCategory.toLowerCase()}-${idNumber}-*.ts`,
|
|
76
|
+
value: filename,
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
return { valid: errors.length === 0, errors };
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Validate a check object has all required fields
|
|
83
|
+
*/
|
|
84
|
+
export function validateCheck(check, filename) {
|
|
85
|
+
const errors = [];
|
|
86
|
+
if (!check || typeof check !== 'object') {
|
|
87
|
+
errors.push({
|
|
88
|
+
field: 'check',
|
|
89
|
+
message: 'Check must be an object',
|
|
90
|
+
value: check,
|
|
91
|
+
});
|
|
92
|
+
return { valid: false, errors };
|
|
93
|
+
}
|
|
94
|
+
const c = check;
|
|
95
|
+
// Validate id
|
|
96
|
+
if (typeof c.id !== 'string' || !c.id) {
|
|
97
|
+
errors.push({
|
|
98
|
+
field: 'id',
|
|
99
|
+
message: 'Check must have a non-empty string id',
|
|
100
|
+
value: c.id,
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
const idResult = validateCheckId(c.id);
|
|
105
|
+
errors.push(...idResult.errors);
|
|
106
|
+
}
|
|
107
|
+
// Validate severity
|
|
108
|
+
if (typeof c.severity !== 'string' || !VALID_SEVERITIES.includes(c.severity)) {
|
|
109
|
+
errors.push({
|
|
110
|
+
field: 'severity',
|
|
111
|
+
message: `Check must have a valid severity: ${VALID_SEVERITIES.join(', ')}`,
|
|
112
|
+
value: c.severity,
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
// Validate name
|
|
116
|
+
if (typeof c.name !== 'string' || !c.name) {
|
|
117
|
+
errors.push({
|
|
118
|
+
field: 'name',
|
|
119
|
+
message: 'Check must have a non-empty string name',
|
|
120
|
+
value: c.name,
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
// Validate execute function
|
|
124
|
+
if (typeof c.execute !== 'function') {
|
|
125
|
+
errors.push({
|
|
126
|
+
field: 'execute',
|
|
127
|
+
message: 'Check must have an execute function',
|
|
128
|
+
value: typeof c.execute,
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
// Validate filename matches check ID if both provided
|
|
132
|
+
if (filename && typeof c.id === 'string' && c.id) {
|
|
133
|
+
const filenameResult = validateFilename(filename, c.id);
|
|
134
|
+
errors.push(...filenameResult.errors);
|
|
135
|
+
}
|
|
136
|
+
return { valid: errors.length === 0, errors };
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Format validation errors for display
|
|
140
|
+
*/
|
|
141
|
+
export function formatValidationErrors(errors, filename) {
|
|
142
|
+
const prefix = filename ? `[${filename}] ` : '';
|
|
143
|
+
return errors.map(e => `${prefix}${e.field}: ${e.message}`).join('\n');
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Validate a check and throw if invalid
|
|
147
|
+
*/
|
|
148
|
+
export function assertValidCheck(check, filename) {
|
|
149
|
+
const result = validateCheck(check, filename);
|
|
150
|
+
if (!result.valid) {
|
|
151
|
+
throw new Error(`Invalid check definition:\n${formatValidationErrors(result.errors, filename)}`);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Extract category from check ID
|
|
156
|
+
*/
|
|
157
|
+
export function getCategoryFromId(id) {
|
|
158
|
+
const match = id.match(CHECK_ID_PATTERN);
|
|
159
|
+
if (!match)
|
|
160
|
+
return null;
|
|
161
|
+
const category = match[1];
|
|
162
|
+
return VALID_CATEGORIES.includes(category) ? category : null;
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Extract number from check ID (e.g., "001" from "NET-001")
|
|
166
|
+
*/
|
|
167
|
+
export function getNumberFromId(id) {
|
|
168
|
+
const match = id.match(CHECK_ID_PATTERN);
|
|
169
|
+
return match ? match[2] : null;
|
|
170
|
+
}
|
|
171
|
+
//# sourceMappingURL=schema.js.map
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SEC-001: API keys hardcoded in config
|
|
3
|
+
*
|
|
4
|
+
* Detects when API keys are hardcoded directly in the configuration
|
|
5
|
+
* file instead of using environment variables.
|
|
6
|
+
*/
|
|
7
|
+
import type { Check } from './types.js';
|
|
8
|
+
declare const check: Check;
|
|
9
|
+
export default check;
|
|
10
|
+
//# sourceMappingURL=sec-001-hardcoded-keys.d.ts.map
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SEC-001: API keys hardcoded in config
|
|
3
|
+
*
|
|
4
|
+
* Detects when API keys are hardcoded directly in the configuration
|
|
5
|
+
* file instead of using environment variables.
|
|
6
|
+
*/
|
|
7
|
+
import { findHardcodedApiKeys, redactValue } from '../utils.js';
|
|
8
|
+
const check = {
|
|
9
|
+
id: 'SEC-001',
|
|
10
|
+
severity: 'HIGH',
|
|
11
|
+
name: 'API keys hardcoded in config',
|
|
12
|
+
execute(ctx) {
|
|
13
|
+
const found = findHardcodedApiKeys(ctx.config);
|
|
14
|
+
if (found.length > 0) {
|
|
15
|
+
return found.map(({ key, value, name }) => ({
|
|
16
|
+
id: 'SEC-001',
|
|
17
|
+
severity: 'HIGH',
|
|
18
|
+
name: 'API keys hardcoded in config',
|
|
19
|
+
location: { file: ctx.configPath, path: key },
|
|
20
|
+
currentValue: redactValue(value),
|
|
21
|
+
expectedValue: 'Use environment variable or secrets manager',
|
|
22
|
+
risk: `${name} is hardcoded in the config file. If the config is committed to version control or exposed, the key can be stolen and abused.`,
|
|
23
|
+
fix: {
|
|
24
|
+
description: 'Move API key to environment variable',
|
|
25
|
+
command: `# Add to .env file:\necho '${key}=${redactValue(value)}' >> ~/.clawdbot/.env\n# Remove from config:\njq 'del(.${key})' ${ctx.configPath} > tmp.json && mv tmp.json ${ctx.configPath}`,
|
|
26
|
+
},
|
|
27
|
+
references: ['https://docs.openclaw.ai/configuration/secrets'],
|
|
28
|
+
}));
|
|
29
|
+
}
|
|
30
|
+
return [];
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
export default check;
|
|
34
|
+
//# sourceMappingURL=sec-001-hardcoded-keys.js.map
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SEC-002: Configuration file is world-readable
|
|
3
|
+
*
|
|
4
|
+
* Detects when the config file has permissions that allow other users
|
|
5
|
+
* on the system to read it.
|
|
6
|
+
*/
|
|
7
|
+
import type { Check } from './types.js';
|
|
8
|
+
declare const check: Check;
|
|
9
|
+
export default check;
|
|
10
|
+
//# sourceMappingURL=sec-002-world-readable-config.d.ts.map
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SEC-002: Configuration file is world-readable
|
|
3
|
+
*
|
|
4
|
+
* Detects when the config file has permissions that allow other users
|
|
5
|
+
* on the system to read it.
|
|
6
|
+
*/
|
|
7
|
+
import { getFileMode, formatMode } from '../utils.js';
|
|
8
|
+
const check = {
|
|
9
|
+
id: 'SEC-002',
|
|
10
|
+
severity: 'HIGH',
|
|
11
|
+
name: 'Configuration file is world-readable',
|
|
12
|
+
execute(ctx) {
|
|
13
|
+
const mode = getFileMode(ctx.configPath);
|
|
14
|
+
// Skip on Windows
|
|
15
|
+
if (mode === null)
|
|
16
|
+
return [];
|
|
17
|
+
// Check if group or other has any permissions
|
|
18
|
+
const groupOther = mode & 0o077;
|
|
19
|
+
if (groupOther > 0) {
|
|
20
|
+
return [{
|
|
21
|
+
id: 'SEC-002',
|
|
22
|
+
severity: 'HIGH',
|
|
23
|
+
name: 'Configuration file is world-readable',
|
|
24
|
+
location: { file: ctx.configPath, path: null },
|
|
25
|
+
currentValue: formatMode(mode),
|
|
26
|
+
expectedValue: '600',
|
|
27
|
+
risk: 'Other users on the system can read sensitive configuration including potential credentials and security settings.',
|
|
28
|
+
fix: {
|
|
29
|
+
description: 'Restrict file permissions',
|
|
30
|
+
command: `chmod 600 ${ctx.configPath}`,
|
|
31
|
+
},
|
|
32
|
+
references: [],
|
|
33
|
+
}];
|
|
34
|
+
}
|
|
35
|
+
return [];
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
export default check;
|
|
39
|
+
//# sourceMappingURL=sec-002-world-readable-config.js.map
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SEC-003: Credentials directory exposed
|
|
3
|
+
*
|
|
4
|
+
* Detects when the credentials directory has permissions that allow
|
|
5
|
+
* other users on the system to access it.
|
|
6
|
+
*/
|
|
7
|
+
import type { Check } from './types.js';
|
|
8
|
+
declare const check: Check;
|
|
9
|
+
export default check;
|
|
10
|
+
//# sourceMappingURL=sec-003-credentials-exposed.d.ts.map
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SEC-003: Credentials directory exposed
|
|
3
|
+
*
|
|
4
|
+
* Detects when the credentials directory has permissions that allow
|
|
5
|
+
* other users on the system to access it.
|
|
6
|
+
*/
|
|
7
|
+
import { getFileMode, formatMode } from '../utils.js';
|
|
8
|
+
import { join } from 'node:path';
|
|
9
|
+
const check = {
|
|
10
|
+
id: 'SEC-003',
|
|
11
|
+
severity: 'HIGH',
|
|
12
|
+
name: 'Credentials directory exposed',
|
|
13
|
+
execute(ctx) {
|
|
14
|
+
const credentialsDir = join(ctx.configDir, 'credentials');
|
|
15
|
+
const mode = getFileMode(credentialsDir);
|
|
16
|
+
// Skip on Windows or if dir doesn't exist
|
|
17
|
+
if (mode === null)
|
|
18
|
+
return [];
|
|
19
|
+
// Check if group or other has any permissions
|
|
20
|
+
const groupOther = mode & 0o077;
|
|
21
|
+
if (groupOther > 0) {
|
|
22
|
+
return [{
|
|
23
|
+
id: 'SEC-003',
|
|
24
|
+
severity: 'HIGH',
|
|
25
|
+
name: 'Credentials directory exposed',
|
|
26
|
+
location: { file: credentialsDir, path: null },
|
|
27
|
+
currentValue: formatMode(mode),
|
|
28
|
+
expectedValue: '700',
|
|
29
|
+
risk: 'Other users on the system can access the credentials directory, potentially reading or modifying stored credentials.',
|
|
30
|
+
fix: {
|
|
31
|
+
description: 'Restrict directory permissions',
|
|
32
|
+
command: `chmod 700 ${credentialsDir}`,
|
|
33
|
+
},
|
|
34
|
+
references: [],
|
|
35
|
+
}];
|
|
36
|
+
}
|
|
37
|
+
return [];
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
export default check;
|
|
41
|
+
//# sourceMappingURL=sec-003-credentials-exposed.js.map
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SEC-004: .env file is readable by other users
|
|
3
|
+
*
|
|
4
|
+
* Detects when the .env file has permissions that allow other users
|
|
5
|
+
* on the system to read it.
|
|
6
|
+
*/
|
|
7
|
+
import type { Check } from './types.js';
|
|
8
|
+
declare const check: Check;
|
|
9
|
+
export default check;
|
|
10
|
+
//# sourceMappingURL=sec-004-env-readable.d.ts.map
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SEC-004: .env file is readable by other users
|
|
3
|
+
*
|
|
4
|
+
* Detects when the .env file has permissions that allow other users
|
|
5
|
+
* on the system to read it.
|
|
6
|
+
*/
|
|
7
|
+
import { getFileMode, formatMode } from '../utils.js';
|
|
8
|
+
import { join } from 'node:path';
|
|
9
|
+
const check = {
|
|
10
|
+
id: 'SEC-004',
|
|
11
|
+
severity: 'MEDIUM',
|
|
12
|
+
name: '.env file is readable by other users',
|
|
13
|
+
execute(ctx) {
|
|
14
|
+
const envPath = join(ctx.configDir, '.env');
|
|
15
|
+
const mode = getFileMode(envPath);
|
|
16
|
+
// Skip on Windows or if file doesn't exist
|
|
17
|
+
if (mode === null)
|
|
18
|
+
return [];
|
|
19
|
+
const groupOther = mode & 0o077;
|
|
20
|
+
if (groupOther > 0) {
|
|
21
|
+
return [{
|
|
22
|
+
id: 'SEC-004',
|
|
23
|
+
severity: 'MEDIUM',
|
|
24
|
+
name: '.env file is readable by other users',
|
|
25
|
+
location: { file: envPath, path: null },
|
|
26
|
+
currentValue: formatMode(mode),
|
|
27
|
+
expectedValue: '600',
|
|
28
|
+
risk: 'The .env file may contain API keys and secrets. Other users on the system can read these credentials.',
|
|
29
|
+
fix: {
|
|
30
|
+
description: 'Restrict file permissions',
|
|
31
|
+
command: `chmod 600 ${envPath}`,
|
|
32
|
+
},
|
|
33
|
+
references: [],
|
|
34
|
+
}];
|
|
35
|
+
}
|
|
36
|
+
return [];
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
export default check;
|
|
40
|
+
//# sourceMappingURL=sec-004-env-readable.js.map
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SEC-005: Session transcripts exposed
|
|
3
|
+
*
|
|
4
|
+
* Detects when the sessions directory in ~/.clawdbot/agents/<agent>/sessions/
|
|
5
|
+
* has permissions other than 700, potentially exposing conversation data.
|
|
6
|
+
* Checking directory permissions is more efficient than checking individual files.
|
|
7
|
+
*/
|
|
8
|
+
import type { Check } from './types.js';
|
|
9
|
+
declare const check: Check;
|
|
10
|
+
export default check;
|
|
11
|
+
//# sourceMappingURL=sec-005-transcripts-exposed.d.ts.map
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SEC-005: Session transcripts exposed
|
|
3
|
+
*
|
|
4
|
+
* Detects when the sessions directory in ~/.clawdbot/agents/<agent>/sessions/
|
|
5
|
+
* has permissions other than 700, potentially exposing conversation data.
|
|
6
|
+
* Checking directory permissions is more efficient than checking individual files.
|
|
7
|
+
*/
|
|
8
|
+
import { getFileMode, formatMode } from '../utils.js';
|
|
9
|
+
import { homedir } from 'node:os';
|
|
10
|
+
import { join } from 'node:path';
|
|
11
|
+
import { existsSync, readdirSync } from 'node:fs';
|
|
12
|
+
const check = {
|
|
13
|
+
id: 'SEC-005',
|
|
14
|
+
severity: 'MEDIUM',
|
|
15
|
+
name: 'Session transcripts exposed',
|
|
16
|
+
execute(ctx) {
|
|
17
|
+
const findings = [];
|
|
18
|
+
const agentsDir = join(homedir(), '.clawdbot', 'agents');
|
|
19
|
+
// Skip if agents directory doesn't exist
|
|
20
|
+
if (!existsSync(agentsDir))
|
|
21
|
+
return [];
|
|
22
|
+
let agentDirs;
|
|
23
|
+
try {
|
|
24
|
+
agentDirs = readdirSync(agentsDir, { withFileTypes: true })
|
|
25
|
+
.filter(d => d.isDirectory())
|
|
26
|
+
.map(d => d.name);
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return [];
|
|
30
|
+
}
|
|
31
|
+
for (const agentName of agentDirs) {
|
|
32
|
+
const sessionsDir = join(agentsDir, agentName, 'sessions');
|
|
33
|
+
if (!existsSync(sessionsDir))
|
|
34
|
+
continue;
|
|
35
|
+
const mode = getFileMode(sessionsDir);
|
|
36
|
+
// Skip on Windows
|
|
37
|
+
if (mode === null)
|
|
38
|
+
continue;
|
|
39
|
+
// Check if group or other has any permissions (should be 700)
|
|
40
|
+
const groupOther = mode & 0o077;
|
|
41
|
+
if (groupOther > 0) {
|
|
42
|
+
findings.push({
|
|
43
|
+
id: 'SEC-005',
|
|
44
|
+
severity: 'MEDIUM',
|
|
45
|
+
name: 'Session transcripts exposed',
|
|
46
|
+
location: { file: sessionsDir, path: null },
|
|
47
|
+
currentValue: formatMode(mode),
|
|
48
|
+
expectedValue: '700',
|
|
49
|
+
risk: 'Sessions directory is accessible by other users. This directory contains conversation history which may include sensitive information.',
|
|
50
|
+
fix: {
|
|
51
|
+
description: 'Restrict directory permissions',
|
|
52
|
+
command: `chmod 700 "${sessionsDir}"`,
|
|
53
|
+
},
|
|
54
|
+
references: ['https://docs.openclaw.ai/security#transcript-protection'],
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return findings;
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
export default check;
|
|
62
|
+
//# sourceMappingURL=sec-005-transcripts-exposed.js.map
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SEC-006: Log redaction disabled
|
|
3
|
+
*
|
|
4
|
+
* Detects when logging.redactSensitive is set to 'off',
|
|
5
|
+
* allowing sensitive information to appear in logs.
|
|
6
|
+
*/
|
|
7
|
+
import type { Check } from './types.js';
|
|
8
|
+
declare const check: Check;
|
|
9
|
+
export default check;
|
|
10
|
+
//# sourceMappingURL=sec-006-redaction-disabled.d.ts.map
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SEC-006: Log redaction disabled
|
|
3
|
+
*
|
|
4
|
+
* Detects when logging.redactSensitive is set to 'off',
|
|
5
|
+
* allowing sensitive information to appear in logs.
|
|
6
|
+
*/
|
|
7
|
+
import { getValueAtPath } from '../utils.js';
|
|
8
|
+
const check = {
|
|
9
|
+
id: 'SEC-006',
|
|
10
|
+
severity: 'LOW',
|
|
11
|
+
name: 'Log redaction disabled',
|
|
12
|
+
execute(ctx) {
|
|
13
|
+
const redactSensitive = getValueAtPath(ctx.config, 'logging.redactSensitive');
|
|
14
|
+
if (redactSensitive === 'off' || redactSensitive === false) {
|
|
15
|
+
return [{
|
|
16
|
+
id: 'SEC-006',
|
|
17
|
+
severity: 'LOW',
|
|
18
|
+
name: 'Log redaction disabled',
|
|
19
|
+
location: { file: ctx.configPath, path: 'logging.redactSensitive' },
|
|
20
|
+
currentValue: redactSensitive,
|
|
21
|
+
expectedValue: 'on (or not set)',
|
|
22
|
+
risk: 'Sensitive information such as API keys, tokens, and personal data may appear in logs unredacted, potentially exposing secrets.',
|
|
23
|
+
fix: {
|
|
24
|
+
description: 'Enable log redaction',
|
|
25
|
+
command: `jq '.logging.redactSensitive = "on"' ${ctx.configPath} > tmp.json && mv tmp.json ${ctx.configPath}`,
|
|
26
|
+
},
|
|
27
|
+
references: ['https://docs.openclaw.ai/logging/security#redaction'],
|
|
28
|
+
}];
|
|
29
|
+
}
|
|
30
|
+
return [];
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
export default check;
|
|
34
|
+
//# sourceMappingURL=sec-006-redaction-disabled.js.map
|
|
@@ -0,0 +1,10 @@
|
|
|
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 type { Check } from './types.js';
|
|
8
|
+
declare const check: Check;
|
|
9
|
+
export default check;
|
|
10
|
+
//# sourceMappingURL=sec-007-no-redact-patterns.d.ts.map
|