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.
@@ -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
+ }