shellward 0.3.2
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 +190 -0
- package/README.md +319 -0
- package/install.ps1 +84 -0
- package/install.sh +201 -0
- package/openclaw.plugin.json +47 -0
- package/package.json +39 -0
- package/skills/security-guide/SKILL.md +68 -0
- package/src/audit-log.ts +57 -0
- package/src/commands/audit.ts +79 -0
- package/src/commands/check-updates.ts +151 -0
- package/src/commands/harden.ts +192 -0
- package/src/commands/index.ts +57 -0
- package/src/commands/scan-plugins.ts +187 -0
- package/src/commands/security.ts +113 -0
- package/src/index.ts +119 -0
- package/src/layers/data-flow-guard.ts +159 -0
- package/src/layers/input-auditor.ts +171 -0
- package/src/layers/outbound-guard.ts +67 -0
- package/src/layers/output-scanner.ts +94 -0
- package/src/layers/prompt-guard.ts +71 -0
- package/src/layers/security-gate.ts +131 -0
- package/src/layers/session-guard.ts +46 -0
- package/src/layers/tool-blocker.ts +182 -0
- package/src/rules/dangerous-commands.ts +105 -0
- package/src/rules/injection-en.ts +102 -0
- package/src/rules/injection-zh.ts +99 -0
- package/src/rules/protected-paths.ts +78 -0
- package/src/rules/sensitive-patterns.ts +204 -0
- package/src/types.ts +93 -0
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
// src/rules/sensitive-patterns.ts — PII & secret patterns for output redaction (global + China)
|
|
2
|
+
|
|
3
|
+
import type { NamedPattern, ScanMatch } from '../types'
|
|
4
|
+
|
|
5
|
+
export interface SensitivePattern {
|
|
6
|
+
id: string
|
|
7
|
+
name: string
|
|
8
|
+
regex: RegExp
|
|
9
|
+
replacement: string
|
|
10
|
+
validate?: (match: string) => boolean
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const SENSITIVE_PATTERNS: SensitivePattern[] = [
|
|
14
|
+
// ===== API Keys & Tokens =====
|
|
15
|
+
{
|
|
16
|
+
id: 'openai_key',
|
|
17
|
+
name: 'OpenAI API Key',
|
|
18
|
+
regex: /sk-[a-zA-Z0-9]{20,}/g,
|
|
19
|
+
replacement: '[REDACTED:OpenAI Key]',
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
id: 'anthropic_key',
|
|
23
|
+
name: 'Anthropic Key',
|
|
24
|
+
regex: /sk-ant-[a-zA-Z0-9\-]{20,}/g,
|
|
25
|
+
replacement: '[REDACTED:Anthropic Key]',
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
id: 'aws_access',
|
|
29
|
+
name: 'AWS Access Key',
|
|
30
|
+
regex: /AKIA[0-9A-Z]{16}/g,
|
|
31
|
+
replacement: '[REDACTED:AWS Key]',
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
id: 'github_token',
|
|
35
|
+
name: 'GitHub Token',
|
|
36
|
+
regex: /gh[ps]_[A-Za-z0-9_]{36,}/g,
|
|
37
|
+
replacement: '[REDACTED:GitHub Token]',
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
id: 'generic_api_key',
|
|
41
|
+
name: 'Generic API Key',
|
|
42
|
+
regex: /(?:api[_-]?key|api[_-]?token|access[_-]?token)\s*[=:]\s*['"]?[A-Za-z0-9_\-]{20,}['"]?/gi,
|
|
43
|
+
replacement: '[REDACTED:API Key]',
|
|
44
|
+
},
|
|
45
|
+
|
|
46
|
+
// ===== Private Keys & Secrets =====
|
|
47
|
+
{
|
|
48
|
+
id: 'private_key',
|
|
49
|
+
name: 'Private Key',
|
|
50
|
+
regex: /-----BEGIN\s(?:RSA|EC|OPENSSH|DSA|PGP)\sPRIVATE\sKEY-----/g,
|
|
51
|
+
replacement: '[REDACTED:Private Key]',
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
id: 'jwt',
|
|
55
|
+
name: 'JWT Token',
|
|
56
|
+
regex: /eyJ[A-Za-z0-9_-]{10,}\.eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}/g,
|
|
57
|
+
replacement: '[REDACTED:JWT]',
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
id: 'password',
|
|
61
|
+
name: 'Password',
|
|
62
|
+
regex: /(?:password|passwd|pwd)\s*[=:]\s*['"]?\S{6,}['"]?/gi,
|
|
63
|
+
replacement: '[REDACTED:Password]',
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
id: 'conn_string',
|
|
67
|
+
name: 'Database Connection String',
|
|
68
|
+
regex: /(?:mongodb|postgres|mysql|redis|amqp):\/\/[^\s'"]{10,}/g,
|
|
69
|
+
replacement: '[REDACTED:Connection String]',
|
|
70
|
+
},
|
|
71
|
+
|
|
72
|
+
// ===== Chinese PII (核心差异点) =====
|
|
73
|
+
{
|
|
74
|
+
id: 'id_card_cn',
|
|
75
|
+
name: '身份证号 / CN ID Card',
|
|
76
|
+
regex: /(?<!\d)[1-9]\d{5}(?:19|20)\d{2}(?:0[1-9]|1[0-2])(?:0[1-9]|[12]\d|3[01])\d{3}[\dXx](?!\d)/g,
|
|
77
|
+
replacement: '[REDACTED:身份证号]',
|
|
78
|
+
validate: validateIdCardCN,
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
id: 'phone_cn',
|
|
82
|
+
name: '手机号 / CN Phone',
|
|
83
|
+
regex: /(?<!\d)1[3-9]\d{9}(?!\d)/g,
|
|
84
|
+
replacement: '[REDACTED:手机号]',
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
id: 'bank_card_cn',
|
|
88
|
+
name: '银行卡号 / CN Bank Card',
|
|
89
|
+
regex: /(?<!\d)(?:62|4|5[1-5])\d{14,17}(?!\d)/g,
|
|
90
|
+
replacement: '[REDACTED:银行卡号]',
|
|
91
|
+
validate: validateLuhn,
|
|
92
|
+
},
|
|
93
|
+
|
|
94
|
+
// ===== International PII =====
|
|
95
|
+
{
|
|
96
|
+
id: 'email',
|
|
97
|
+
name: 'Email Address',
|
|
98
|
+
regex: /[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}/g,
|
|
99
|
+
replacement: '[REDACTED:Email]',
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
id: 'ssn_us',
|
|
103
|
+
name: 'US SSN',
|
|
104
|
+
// Exclude date-like patterns (YYYY-MM-DD) and ranges starting with 000/666/9xx
|
|
105
|
+
regex: /\b(?!000|666|9\d\d)\d{3}-(?!00)\d{2}-(?!0000)\d{4}\b/g,
|
|
106
|
+
replacement: '[REDACTED:SSN]',
|
|
107
|
+
validate: validateSSN,
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
id: 'credit_card',
|
|
111
|
+
name: 'Credit Card',
|
|
112
|
+
regex: /\b(?:4\d{3}|5[1-5]\d{2}|3[47]\d{2}|6(?:011|5\d{2}))[- ]?\d{4}[- ]?\d{4}[- ]?\d{4}\b/g,
|
|
113
|
+
replacement: '[REDACTED:Credit Card]',
|
|
114
|
+
validate: validateLuhn,
|
|
115
|
+
},
|
|
116
|
+
]
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Scan text and return matches (without modifying text).
|
|
120
|
+
*/
|
|
121
|
+
export function scanForSensitive(text: string): ScanMatch[] {
|
|
122
|
+
const results: ScanMatch[] = []
|
|
123
|
+
for (const pat of SENSITIVE_PATTERNS) {
|
|
124
|
+
const regex = new RegExp(pat.regex.source, pat.regex.flags)
|
|
125
|
+
let match: RegExpExecArray | null
|
|
126
|
+
while ((match = regex.exec(text)) !== null) {
|
|
127
|
+
if (pat.validate && !pat.validate(match[0])) continue
|
|
128
|
+
results.push({
|
|
129
|
+
name: pat.name,
|
|
130
|
+
preview: match[0].slice(0, 8) + '***',
|
|
131
|
+
})
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return results
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Redact all sensitive data in text. Returns [redactedText, findings[]]
|
|
139
|
+
*/
|
|
140
|
+
export function redactSensitive(text: string): [string, { id: string; name: string; count: number }[]] {
|
|
141
|
+
let result = text
|
|
142
|
+
const findings: { id: string; name: string; count: number }[] = []
|
|
143
|
+
|
|
144
|
+
for (const pat of SENSITIVE_PATTERNS) {
|
|
145
|
+
const regex = new RegExp(pat.regex.source, pat.regex.flags)
|
|
146
|
+
let count = 0
|
|
147
|
+
result = result.replace(regex, (match) => {
|
|
148
|
+
if (pat.validate && !pat.validate(match)) return match
|
|
149
|
+
count++
|
|
150
|
+
return pat.replacement
|
|
151
|
+
})
|
|
152
|
+
if (count > 0) {
|
|
153
|
+
findings.push({ id: pat.id, name: pat.name, count })
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return [result, findings]
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// ===== Validators =====
|
|
161
|
+
|
|
162
|
+
function validateIdCardCN(id: string): boolean {
|
|
163
|
+
if (id.length !== 18) return false
|
|
164
|
+
const weights = [7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2]
|
|
165
|
+
const checkCodes = ['1', '0', 'X', '9', '8', '7', '6', '5', '4', '3', '2']
|
|
166
|
+
let sum = 0
|
|
167
|
+
for (let i = 0; i < 17; i++) {
|
|
168
|
+
sum += parseInt(id[i]) * weights[i]
|
|
169
|
+
}
|
|
170
|
+
return checkCodes[sum % 11].toUpperCase() === id[17].toUpperCase()
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Validate US SSN: reject date-like patterns (YYYY-MM-DD)
|
|
175
|
+
*/
|
|
176
|
+
function validateSSN(ssn: string): boolean {
|
|
177
|
+
const parts = ssn.split('-')
|
|
178
|
+
if (parts.length !== 3) return false
|
|
179
|
+
const [area, group, serial] = parts.map(Number)
|
|
180
|
+
// Reject if it looks like a date (first part 1900-2099)
|
|
181
|
+
if (area >= 1900 && area <= 2099 && group >= 1 && group <= 12) return false
|
|
182
|
+
// Valid SSN ranges
|
|
183
|
+
if (area < 1 || area > 899 || area === 666) return false
|
|
184
|
+
if (group < 1 || group > 99) return false
|
|
185
|
+
if (serial < 1 || serial > 9999) return false
|
|
186
|
+
return true
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function validateLuhn(num: string): boolean {
|
|
190
|
+
const digits = num.replace(/\D/g, '')
|
|
191
|
+
if (digits.length < 13) return false
|
|
192
|
+
let sum = 0
|
|
193
|
+
let alternate = false
|
|
194
|
+
for (let i = digits.length - 1; i >= 0; i--) {
|
|
195
|
+
let n = parseInt(digits[i])
|
|
196
|
+
if (alternate) {
|
|
197
|
+
n *= 2
|
|
198
|
+
if (n > 9) n -= 9
|
|
199
|
+
}
|
|
200
|
+
sum += n
|
|
201
|
+
alternate = !alternate
|
|
202
|
+
}
|
|
203
|
+
return sum % 10 === 0
|
|
204
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
// src/types.ts — ShellWard type definitions
|
|
2
|
+
|
|
3
|
+
export interface ShellWardConfig {
|
|
4
|
+
mode: 'enforce' | 'audit'
|
|
5
|
+
locale: 'auto' | 'zh' | 'en'
|
|
6
|
+
layers: {
|
|
7
|
+
promptGuard: boolean
|
|
8
|
+
outputScanner: boolean
|
|
9
|
+
toolBlocker: boolean
|
|
10
|
+
inputAuditor: boolean
|
|
11
|
+
securityGate: boolean
|
|
12
|
+
outboundGuard: boolean
|
|
13
|
+
dataFlowGuard: boolean
|
|
14
|
+
sessionGuard: boolean
|
|
15
|
+
}
|
|
16
|
+
injectionThreshold: number
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export type ResolvedLocale = 'zh' | 'en'
|
|
20
|
+
|
|
21
|
+
export interface AuditEntry {
|
|
22
|
+
ts: string
|
|
23
|
+
level: 'CRITICAL' | 'HIGH' | 'MEDIUM' | 'LOW' | 'INFO'
|
|
24
|
+
layer: 'L1' | 'L2' | 'L3' | 'L4' | 'L5' | 'L6' | 'L7' | 'L8'
|
|
25
|
+
action: 'block' | 'redact' | 'detect' | 'allow' | 'inject'
|
|
26
|
+
detail: string
|
|
27
|
+
tool?: string
|
|
28
|
+
pattern?: string
|
|
29
|
+
mode: 'enforce' | 'audit'
|
|
30
|
+
[key: string]: unknown
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface NamedPattern {
|
|
34
|
+
name: string
|
|
35
|
+
pattern: RegExp
|
|
36
|
+
validate?: (match: string) => boolean
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface ScanMatch {
|
|
40
|
+
name: string
|
|
41
|
+
preview: string
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface DangerousCommandRule {
|
|
45
|
+
id: string
|
|
46
|
+
pattern: RegExp
|
|
47
|
+
description_zh: string
|
|
48
|
+
description_en: string
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface ProtectedPathRule {
|
|
52
|
+
id: string
|
|
53
|
+
pattern: RegExp
|
|
54
|
+
description_zh: string
|
|
55
|
+
description_en: string
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface InjectionRule {
|
|
59
|
+
id: string
|
|
60
|
+
name: string
|
|
61
|
+
pattern: string
|
|
62
|
+
flags?: string
|
|
63
|
+
riskScore: number
|
|
64
|
+
category: string
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export const DEFAULT_CONFIG: ShellWardConfig = {
|
|
68
|
+
mode: 'enforce',
|
|
69
|
+
locale: 'auto',
|
|
70
|
+
layers: {
|
|
71
|
+
promptGuard: true,
|
|
72
|
+
outputScanner: true,
|
|
73
|
+
toolBlocker: true,
|
|
74
|
+
inputAuditor: true,
|
|
75
|
+
securityGate: true,
|
|
76
|
+
outboundGuard: true,
|
|
77
|
+
dataFlowGuard: true,
|
|
78
|
+
sessionGuard: true,
|
|
79
|
+
},
|
|
80
|
+
injectionThreshold: 60,
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Detect locale from system environment.
|
|
85
|
+
* Returns 'zh' if LANG/LC_ALL contains 'zh', otherwise 'en'.
|
|
86
|
+
*/
|
|
87
|
+
export function resolveLocale(config: ShellWardConfig): ResolvedLocale {
|
|
88
|
+
if (config.locale === 'zh') return 'zh'
|
|
89
|
+
if (config.locale === 'en') return 'en'
|
|
90
|
+
// auto detection
|
|
91
|
+
const lang = process.env.LANG || process.env.LANGUAGE || process.env.LC_ALL || ''
|
|
92
|
+
return /zh/i.test(lang) ? 'zh' : 'en'
|
|
93
|
+
}
|