@weave_protocol/mund 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/dist/analyzers/code-analyzer.d.ts +24 -0
- package/dist/analyzers/code-analyzer.d.ts.map +1 -0
- package/dist/analyzers/code-analyzer.js +73 -0
- package/dist/analyzers/code-analyzer.js.map +1 -0
- package/dist/analyzers/exfiltration-detector.d.ts +40 -0
- package/dist/analyzers/exfiltration-detector.d.ts.map +1 -0
- package/dist/analyzers/exfiltration-detector.js +220 -0
- package/dist/analyzers/exfiltration-detector.js.map +1 -0
- package/dist/analyzers/index.d.ts +26 -0
- package/dist/analyzers/index.d.ts.map +1 -0
- package/dist/analyzers/index.js +61 -0
- package/dist/analyzers/index.js.map +1 -0
- package/dist/analyzers/injection-detector.d.ts +36 -0
- package/dist/analyzers/injection-detector.d.ts.map +1 -0
- package/dist/analyzers/injection-detector.js +165 -0
- package/dist/analyzers/injection-detector.js.map +1 -0
- package/dist/analyzers/pii-detector.d.ts +44 -0
- package/dist/analyzers/pii-detector.d.ts.map +1 -0
- package/dist/analyzers/pii-detector.js +190 -0
- package/dist/analyzers/pii-detector.js.map +1 -0
- package/dist/analyzers/secret-scanner.d.ts +32 -0
- package/dist/analyzers/secret-scanner.d.ts.map +1 -0
- package/dist/analyzers/secret-scanner.js +119 -0
- package/dist/analyzers/secret-scanner.js.map +1 -0
- package/dist/constants.d.ts +18 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +580 -0
- package/dist/constants.js.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +242 -0
- package/dist/index.js.map +1 -0
- package/dist/notifications/email.d.ts +18 -0
- package/dist/notifications/email.d.ts.map +1 -0
- package/dist/notifications/email.js +192 -0
- package/dist/notifications/email.js.map +1 -0
- package/dist/notifications/index.d.ts +51 -0
- package/dist/notifications/index.d.ts.map +1 -0
- package/dist/notifications/index.js +107 -0
- package/dist/notifications/index.js.map +1 -0
- package/dist/notifications/slack.d.ts +17 -0
- package/dist/notifications/slack.d.ts.map +1 -0
- package/dist/notifications/slack.js +128 -0
- package/dist/notifications/slack.js.map +1 -0
- package/dist/notifications/teams.d.ts +17 -0
- package/dist/notifications/teams.d.ts.map +1 -0
- package/dist/notifications/teams.js +136 -0
- package/dist/notifications/teams.js.map +1 -0
- package/dist/notifications/webhook.d.ts +15 -0
- package/dist/notifications/webhook.d.ts.map +1 -0
- package/dist/notifications/webhook.js +67 -0
- package/dist/notifications/webhook.js.map +1 -0
- package/dist/storage/index.d.ts +11 -0
- package/dist/storage/index.d.ts.map +1 -0
- package/dist/storage/index.js +26 -0
- package/dist/storage/index.js.map +1 -0
- package/dist/storage/memory.d.ts +35 -0
- package/dist/storage/memory.d.ts.map +1 -0
- package/dist/storage/memory.js +144 -0
- package/dist/storage/memory.js.map +1 -0
- package/dist/tools/configuration.d.ts +8 -0
- package/dist/tools/configuration.d.ts.map +1 -0
- package/dist/tools/configuration.js +399 -0
- package/dist/tools/configuration.js.map +1 -0
- package/dist/tools/index.d.ts +15 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/index.js +16 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/monitoring.d.ts +10 -0
- package/dist/tools/monitoring.d.ts.map +1 -0
- package/dist/tools/monitoring.js +476 -0
- package/dist/tools/monitoring.js.map +1 -0
- package/dist/types.d.ts +207 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +6 -0
- package/dist/types.js.map +1 -0
- package/package.json +62 -0
- package/rules/default.yaml +189 -0
- package/src/analyzers/code-analyzer.ts +88 -0
- package/src/analyzers/exfiltration-detector.ts +257 -0
- package/src/analyzers/index.ts +74 -0
- package/src/analyzers/injection-detector.ts +191 -0
- package/src/analyzers/pii-detector.ts +220 -0
- package/src/analyzers/secret-scanner.ts +142 -0
- package/src/constants.ts +603 -0
- package/src/index.ts +285 -0
- package/src/notifications/email.ts +211 -0
- package/src/notifications/index.ts +136 -0
- package/src/notifications/slack.ts +170 -0
- package/src/notifications/teams.ts +200 -0
- package/src/notifications/webhook.ts +80 -0
- package/src/storage/index.ts +29 -0
- package/src/storage/memory.ts +181 -0
- package/src/tools/configuration.ts +478 -0
- package/src/tools/index.ts +29 -0
- package/src/tools/monitoring.ts +559 -0
- package/src/types.ts +300 -0
- package/tsconfig.json +24 -0
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mund - The Guardian Protocol
|
|
3
|
+
* PII Detector - Detects personally identifiable information
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
type DetectionRule,
|
|
8
|
+
type SecurityIssue,
|
|
9
|
+
type IAnalyzer,
|
|
10
|
+
type IssueLocation
|
|
11
|
+
} from '../types.js';
|
|
12
|
+
|
|
13
|
+
export class PIIDetector implements IAnalyzer {
|
|
14
|
+
name = 'PIIDetector';
|
|
15
|
+
type = 'pii';
|
|
16
|
+
|
|
17
|
+
async analyze(content: string, rules: DetectionRule[]): Promise<SecurityIssue[]> {
|
|
18
|
+
const issues: SecurityIssue[] = [];
|
|
19
|
+
const piiRules = rules.filter(r => r.type === 'pii' && r.enabled);
|
|
20
|
+
|
|
21
|
+
for (const rule of piiRules) {
|
|
22
|
+
if (!rule.pattern) continue;
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
const regex = new RegExp(rule.pattern, 'g');
|
|
26
|
+
let match: RegExpExecArray | null;
|
|
27
|
+
|
|
28
|
+
while ((match = regex.exec(content)) !== null) {
|
|
29
|
+
const matchText = match[0];
|
|
30
|
+
|
|
31
|
+
// Additional validation for specific types
|
|
32
|
+
if (!this.validateMatch(rule.id, matchText)) {
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const location = this.getLocation(content, match.index, matchText.length);
|
|
37
|
+
|
|
38
|
+
issues.push({
|
|
39
|
+
rule_id: rule.id,
|
|
40
|
+
rule_name: rule.name,
|
|
41
|
+
type: rule.type,
|
|
42
|
+
severity: rule.severity,
|
|
43
|
+
action: rule.action,
|
|
44
|
+
match: this.redactPII(rule.id, matchText),
|
|
45
|
+
location,
|
|
46
|
+
suggestion: this.getSuggestion(rule.id)
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
} catch (error) {
|
|
50
|
+
console.error(`Invalid regex pattern in rule ${rule.id}:`, error);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return issues;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Additional validation for matches to reduce false positives
|
|
59
|
+
*/
|
|
60
|
+
private validateMatch(ruleId: string, match: string): boolean {
|
|
61
|
+
switch (ruleId) {
|
|
62
|
+
case 'ssn_us':
|
|
63
|
+
return this.validateSSN(match);
|
|
64
|
+
case 'credit_card':
|
|
65
|
+
return this.validateCreditCard(match);
|
|
66
|
+
case 'email_address':
|
|
67
|
+
return this.validateEmail(match);
|
|
68
|
+
case 'ip_address':
|
|
69
|
+
return this.validateIPAddress(match);
|
|
70
|
+
default:
|
|
71
|
+
return true;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Validate SSN format (basic validation, not checking if real)
|
|
77
|
+
*/
|
|
78
|
+
private validateSSN(ssn: string): boolean {
|
|
79
|
+
const digits = ssn.replace(/\D/g, '');
|
|
80
|
+
if (digits.length !== 9) return false;
|
|
81
|
+
|
|
82
|
+
// SSNs cannot start with 000, 666, or 900-999
|
|
83
|
+
const area = parseInt(digits.substring(0, 3));
|
|
84
|
+
if (area === 0 || area === 666 || area >= 900) return false;
|
|
85
|
+
|
|
86
|
+
// Group number cannot be 00
|
|
87
|
+
const group = parseInt(digits.substring(3, 5));
|
|
88
|
+
if (group === 0) return false;
|
|
89
|
+
|
|
90
|
+
// Serial number cannot be 0000
|
|
91
|
+
const serial = parseInt(digits.substring(5));
|
|
92
|
+
if (serial === 0) return false;
|
|
93
|
+
|
|
94
|
+
return true;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Validate credit card using Luhn algorithm
|
|
99
|
+
*/
|
|
100
|
+
private validateCreditCard(cc: string): boolean {
|
|
101
|
+
const digits = cc.replace(/\D/g, '');
|
|
102
|
+
if (digits.length < 13 || digits.length > 19) return false;
|
|
103
|
+
|
|
104
|
+
let sum = 0;
|
|
105
|
+
let isEven = false;
|
|
106
|
+
|
|
107
|
+
for (let i = digits.length - 1; i >= 0; i--) {
|
|
108
|
+
let digit = parseInt(digits[i]);
|
|
109
|
+
|
|
110
|
+
if (isEven) {
|
|
111
|
+
digit *= 2;
|
|
112
|
+
if (digit > 9) {
|
|
113
|
+
digit -= 9;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
sum += digit;
|
|
118
|
+
isEven = !isEven;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return sum % 10 === 0;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Validate email address format
|
|
126
|
+
*/
|
|
127
|
+
private validateEmail(email: string): boolean {
|
|
128
|
+
// Check for common false positives
|
|
129
|
+
const falsePaths = ['.js', '.ts', '.py', '.java', '.cpp', '.go', '.rs'];
|
|
130
|
+
if (falsePaths.some(ext => email.endsWith(ext))) return false;
|
|
131
|
+
|
|
132
|
+
// Check for reasonable length
|
|
133
|
+
if (email.length > 254) return false;
|
|
134
|
+
|
|
135
|
+
// Check for valid TLD (at least 2 chars)
|
|
136
|
+
const parts = email.split('.');
|
|
137
|
+
const tld = parts[parts.length - 1];
|
|
138
|
+
if (tld.length < 2) return false;
|
|
139
|
+
|
|
140
|
+
return true;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Validate IP address (exclude obvious non-IPs)
|
|
145
|
+
*/
|
|
146
|
+
private validateIPAddress(ip: string): boolean {
|
|
147
|
+
// Exclude version numbers (e.g., 1.2.3.4 in software versions)
|
|
148
|
+
const parts = ip.split('.');
|
|
149
|
+
|
|
150
|
+
// All parts should be valid octets
|
|
151
|
+
for (const part of parts) {
|
|
152
|
+
const num = parseInt(part);
|
|
153
|
+
if (num < 0 || num > 255) return false;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Exclude common version patterns
|
|
157
|
+
if (parts[0] === '0' || parts[0] === '1') {
|
|
158
|
+
if (parseInt(parts[1]) <= 20 && parseInt(parts[2]) <= 20) {
|
|
159
|
+
return false; // Likely a version number
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Exclude localhost and broadcast
|
|
164
|
+
if (ip === '127.0.0.1' || ip === '0.0.0.0' || ip === '255.255.255.255') {
|
|
165
|
+
return false;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return true;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Calculate location within content
|
|
173
|
+
*/
|
|
174
|
+
private getLocation(content: string, start: number, length: number): IssueLocation {
|
|
175
|
+
const end = start + length;
|
|
176
|
+
const lines = content.substring(0, start).split('\n');
|
|
177
|
+
const line = lines.length;
|
|
178
|
+
const column = lines[lines.length - 1].length + 1;
|
|
179
|
+
return { start, end, line, column };
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Redact PII for safe display
|
|
184
|
+
*/
|
|
185
|
+
private redactPII(ruleId: string, value: string): string {
|
|
186
|
+
switch (ruleId) {
|
|
187
|
+
case 'ssn_us':
|
|
188
|
+
return `***-**-${value.slice(-4)}`;
|
|
189
|
+
case 'credit_card':
|
|
190
|
+
return `****-****-****-${value.slice(-4)}`;
|
|
191
|
+
case 'email_address':
|
|
192
|
+
const [local, domain] = value.split('@');
|
|
193
|
+
return `${local.charAt(0)}***@${domain}`;
|
|
194
|
+
case 'phone_number_us':
|
|
195
|
+
return `***-***-${value.slice(-4)}`;
|
|
196
|
+
case 'ip_address':
|
|
197
|
+
const parts = value.split('.');
|
|
198
|
+
return `${parts[0]}.***.***.*${parts[3].slice(-1)}`;
|
|
199
|
+
default:
|
|
200
|
+
return value.substring(0, 3) + '***' + value.slice(-2);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Get remediation suggestion
|
|
206
|
+
*/
|
|
207
|
+
private getSuggestion(ruleId: string): string {
|
|
208
|
+
const suggestions: Record<string, string> = {
|
|
209
|
+
ssn_us: 'Social Security Numbers should never be stored in plain text. Use encryption or tokenization.',
|
|
210
|
+
credit_card: 'Credit card numbers must be handled according to PCI DSS standards. Use tokenization.',
|
|
211
|
+
email_address: 'Consider whether email addresses need to be logged. Hash or encrypt if stored.',
|
|
212
|
+
phone_number_us: 'Phone numbers are PII. Consider whether they need to be included.',
|
|
213
|
+
ip_address: 'IP addresses can be PII under GDPR. Consider hashing or anonymizing.'
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
return suggestions[ruleId] || 'Review whether this PII needs to be included and handle according to privacy policies.';
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
export default PIIDetector;
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mund - The Guardian Protocol
|
|
3
|
+
* Secret Scanner - Detects secrets, API keys, and credentials
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
type DetectionRule,
|
|
8
|
+
type SecurityIssue,
|
|
9
|
+
type IAnalyzer,
|
|
10
|
+
type IssueLocation
|
|
11
|
+
} from '../types.js';
|
|
12
|
+
|
|
13
|
+
export class SecretScanner implements IAnalyzer {
|
|
14
|
+
name = 'SecretScanner';
|
|
15
|
+
type = 'secret';
|
|
16
|
+
|
|
17
|
+
async analyze(content: string, rules: DetectionRule[]): Promise<SecurityIssue[]> {
|
|
18
|
+
const issues: SecurityIssue[] = [];
|
|
19
|
+
const secretRules = rules.filter(r => r.type === 'secret' && r.enabled);
|
|
20
|
+
|
|
21
|
+
for (const rule of secretRules) {
|
|
22
|
+
if (!rule.pattern) continue;
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
const regex = new RegExp(rule.pattern, 'g');
|
|
26
|
+
let match: RegExpExecArray | null;
|
|
27
|
+
|
|
28
|
+
while ((match = regex.exec(content)) !== null) {
|
|
29
|
+
const matchText = match[0];
|
|
30
|
+
const location = this.getLocation(content, match.index, matchText.length);
|
|
31
|
+
|
|
32
|
+
issues.push({
|
|
33
|
+
rule_id: rule.id,
|
|
34
|
+
rule_name: rule.name,
|
|
35
|
+
type: rule.type,
|
|
36
|
+
severity: rule.severity,
|
|
37
|
+
action: rule.action,
|
|
38
|
+
match: this.redactSecret(matchText),
|
|
39
|
+
location,
|
|
40
|
+
suggestion: this.getSuggestion(rule.id)
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
} catch (error) {
|
|
44
|
+
// Invalid regex pattern - skip this rule
|
|
45
|
+
console.error(`Invalid regex pattern in rule ${rule.id}:`, error);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return issues;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Calculate the location of a match within the content
|
|
54
|
+
*/
|
|
55
|
+
private getLocation(content: string, start: number, length: number): IssueLocation {
|
|
56
|
+
const end = start + length;
|
|
57
|
+
|
|
58
|
+
// Calculate line and column
|
|
59
|
+
const lines = content.substring(0, start).split('\n');
|
|
60
|
+
const line = lines.length;
|
|
61
|
+
const column = lines[lines.length - 1].length + 1;
|
|
62
|
+
|
|
63
|
+
return { start, end, line, column };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Redact the middle portion of a secret for safe display
|
|
68
|
+
*/
|
|
69
|
+
private redactSecret(secret: string): string {
|
|
70
|
+
if (secret.length <= 8) {
|
|
71
|
+
return '****';
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const visibleStart = Math.min(4, Math.floor(secret.length * 0.2));
|
|
75
|
+
const visibleEnd = Math.min(4, Math.floor(secret.length * 0.2));
|
|
76
|
+
const redactedLength = secret.length - visibleStart - visibleEnd;
|
|
77
|
+
|
|
78
|
+
return `${secret.substring(0, visibleStart)}${'*'.repeat(Math.min(redactedLength, 20))}${secret.substring(secret.length - visibleEnd)}`;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Get remediation suggestion for a specific rule
|
|
83
|
+
*/
|
|
84
|
+
private getSuggestion(ruleId: string): string {
|
|
85
|
+
const suggestions: Record<string, string> = {
|
|
86
|
+
aws_access_key_id: 'Use AWS IAM roles or environment variables instead of hardcoding credentials.',
|
|
87
|
+
aws_secret_access_key: 'Never commit AWS secret keys. Use AWS Secrets Manager or environment variables.',
|
|
88
|
+
github_token: 'Use GitHub Actions secrets or environment variables for tokens.',
|
|
89
|
+
github_fine_grained_token: 'Store GitHub tokens in a secrets manager, not in code.',
|
|
90
|
+
openai_api_key: 'Use environment variables (OPENAI_API_KEY) instead of hardcoding.',
|
|
91
|
+
anthropic_api_key: 'Use environment variables (ANTHROPIC_API_KEY) instead of hardcoding.',
|
|
92
|
+
google_api_key: 'Restrict API key usage in Google Cloud Console and use environment variables.',
|
|
93
|
+
slack_token: 'Store Slack tokens in environment variables or a secrets manager.',
|
|
94
|
+
slack_webhook: 'Webhook URLs should be stored in environment variables.',
|
|
95
|
+
stripe_api_key: 'Use environment variables. Never expose live keys in client-side code.',
|
|
96
|
+
private_key_block: 'Private keys should never be in code. Use a key management service.',
|
|
97
|
+
jwt_token: 'JWTs should be transmitted securely and never logged or stored in code.',
|
|
98
|
+
database_url: 'Database credentials should be in environment variables or a secrets manager.',
|
|
99
|
+
generic_api_key: 'API keys should be stored in environment variables or a secrets manager.',
|
|
100
|
+
generic_password: 'Passwords should never be hardcoded. Use a secrets manager.',
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
return suggestions[ruleId] || 'Remove this secret from code and use a secrets manager or environment variables.';
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Calculate Shannon entropy of a string (higher = more random = more likely a secret)
|
|
108
|
+
*/
|
|
109
|
+
static calculateEntropy(str: string): number {
|
|
110
|
+
const len = str.length;
|
|
111
|
+
if (len === 0) return 0;
|
|
112
|
+
|
|
113
|
+
const frequencies: Record<string, number> = {};
|
|
114
|
+
for (const char of str) {
|
|
115
|
+
frequencies[char] = (frequencies[char] || 0) + 1;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
let entropy = 0;
|
|
119
|
+
for (const count of Object.values(frequencies)) {
|
|
120
|
+
const p = count / len;
|
|
121
|
+
entropy -= p * Math.log2(p);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return entropy;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Check if a string is likely a high-entropy secret
|
|
129
|
+
*/
|
|
130
|
+
static isHighEntropy(str: string, threshold = 4.5): boolean {
|
|
131
|
+
// Skip strings that are too short or too long
|
|
132
|
+
if (str.length < 16 || str.length > 200) return false;
|
|
133
|
+
|
|
134
|
+
// Skip if it looks like a common word or path
|
|
135
|
+
if (/^[a-z]+$/i.test(str)) return false;
|
|
136
|
+
if (str.includes('/') && !str.includes('://')) return false;
|
|
137
|
+
|
|
138
|
+
return SecretScanner.calculateEntropy(str) > threshold;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export default SecretScanner;
|