@runhalo/engine 0.3.0 → 0.4.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/index.d.ts +54 -0
- package/dist/index.js +442 -28
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/rules/rules.json +1005 -0
package/dist/index.js
CHANGED
|
@@ -44,8 +44,11 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
44
44
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
45
45
|
};
|
|
46
46
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
47
|
-
exports.SCAFFOLD_REGISTRY = exports.detectFramework = exports.ScaffoldEngine = exports.ComplianceScoreEngine = exports.transformSetDefault = exports.transformSanitizeInput = exports.transformRemoveDefault = exports.transformUrlUpgrade = exports.FixEngine = exports.REMEDIATION_MAP = exports.HaloEngine = exports.ETHICAL_RULES = exports.COPPA_RULES = exports.treeSitterParser = exports.TreeSitterParser = void 0;
|
|
47
|
+
exports.SCAFFOLD_REGISTRY = exports.detectFramework = exports.ScaffoldEngine = exports.ComplianceScoreEngine = exports.transformSetDefault = exports.transformSanitizeInput = exports.transformRemoveDefault = exports.transformUrlUpgrade = exports.FixEngine = exports.REMEDIATION_MAP = exports.HaloEngine = exports.AU_SBD_RULES = exports.AI_AUDIT_RULES = exports.ETHICAL_RULES = exports.COPPA_RULES = exports.treeSitterParser = exports.TreeSitterParser = void 0;
|
|
48
48
|
exports.loadRulesFromYAML = loadRulesFromYAML;
|
|
49
|
+
exports.loadRulesFromJSON = loadRulesFromJSON;
|
|
50
|
+
exports.loadRulesFromJSONByPack = loadRulesFromJSONByPack;
|
|
51
|
+
exports.compileRawRules = compileRawRules;
|
|
49
52
|
exports.parseHaloignore = parseHaloignore;
|
|
50
53
|
exports.shouldIgnoreFile = shouldIgnoreFile;
|
|
51
54
|
exports.shouldIgnoreViolation = shouldIgnoreViolation;
|
|
@@ -56,10 +59,14 @@ const tree_sitter_1 = __importDefault(require("tree-sitter"));
|
|
|
56
59
|
const tree_sitter_typescript_1 = __importDefault(require("tree-sitter-typescript"));
|
|
57
60
|
const tree_sitter_javascript_1 = __importDefault(require("tree-sitter-javascript"));
|
|
58
61
|
const yaml = __importStar(require("js-yaml"));
|
|
59
|
-
// Extract category from ruleId (e.g. "coppa-auth-001" → "auth", "ETHICAL-001" → "ethical")
|
|
62
|
+
// Extract category from ruleId (e.g. "coppa-auth-001" → "auth", "ETHICAL-001" → "ethical", "AU-SBD-001" → "au-sbd")
|
|
60
63
|
function extractCategory(ruleId) {
|
|
61
64
|
if (ruleId.startsWith('ETHICAL'))
|
|
62
65
|
return 'ethical';
|
|
66
|
+
if (ruleId.startsWith('AI-AUDIT'))
|
|
67
|
+
return 'ai-audit';
|
|
68
|
+
if (ruleId.startsWith('AU-SBD'))
|
|
69
|
+
return 'au-sbd';
|
|
63
70
|
const match = ruleId.match(/^coppa-(\w+)-\d+$/);
|
|
64
71
|
return match ? match[1] : 'unknown';
|
|
65
72
|
}
|
|
@@ -71,7 +78,8 @@ function detectLanguage(filePath) {
|
|
|
71
78
|
'.py': 'python', '.swift': 'swift', '.java': 'java', '.kt': 'kotlin',
|
|
72
79
|
'.html': 'html', '.vue': 'vue', '.svelte': 'svelte', '.php': 'php',
|
|
73
80
|
'.cpp': 'cpp', '.h': 'cpp', '.hpp': 'cpp', '.cs': 'csharp',
|
|
74
|
-
'.qml': 'qml', '.sql': 'sql',
|
|
81
|
+
'.qml': 'qml', '.sql': 'sql', '.go': 'go', '.rb': 'ruby',
|
|
82
|
+
'.xml': 'xml', '.erb': 'ruby',
|
|
75
83
|
};
|
|
76
84
|
return langMap[ext] || 'unknown';
|
|
77
85
|
}
|
|
@@ -106,6 +114,71 @@ function loadRulesFromYAML(yamlPath) {
|
|
|
106
114
|
return [];
|
|
107
115
|
}
|
|
108
116
|
}
|
|
117
|
+
// Convert a JSON rule definition to the engine's Rule interface
|
|
118
|
+
function jsonRuleToRule(jsonRule) {
|
|
119
|
+
const patterns = jsonRule.patterns.map(p => new RegExp(p.pattern, p.flags));
|
|
120
|
+
// Map fixability string to Fixability type
|
|
121
|
+
const fixabilityMap = {
|
|
122
|
+
'auto': 'auto',
|
|
123
|
+
'guided': 'guided',
|
|
124
|
+
'flag-only': 'flag-only',
|
|
125
|
+
};
|
|
126
|
+
const remediation = {
|
|
127
|
+
fixability: fixabilityMap[jsonRule.fixability] || 'flag-only',
|
|
128
|
+
transformType: jsonRule.transform_type || undefined,
|
|
129
|
+
scaffoldId: jsonRule.scaffold_id || undefined,
|
|
130
|
+
guidanceUrl: jsonRule.guidance_url || undefined,
|
|
131
|
+
};
|
|
132
|
+
return {
|
|
133
|
+
id: jsonRule.id,
|
|
134
|
+
name: jsonRule.name,
|
|
135
|
+
severity: jsonRule.severity,
|
|
136
|
+
description: jsonRule.description,
|
|
137
|
+
patterns,
|
|
138
|
+
fixSuggestion: jsonRule.fix_suggestion,
|
|
139
|
+
penalty: jsonRule.penalty,
|
|
140
|
+
languages: jsonRule.languages,
|
|
141
|
+
remediation,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
// JSON Rule Loader — Load rules from rules.json
|
|
145
|
+
function loadRulesFromJSON(jsonPath) {
|
|
146
|
+
try {
|
|
147
|
+
const jsonContent = fs.readFileSync(jsonPath, 'utf-8');
|
|
148
|
+
const config = JSON.parse(jsonContent);
|
|
149
|
+
if (!config?.rules || !Array.isArray(config.rules)) {
|
|
150
|
+
console.warn(`No rules found in JSON file: ${jsonPath}`);
|
|
151
|
+
return [];
|
|
152
|
+
}
|
|
153
|
+
return config.rules.map(jsonRuleToRule);
|
|
154
|
+
}
|
|
155
|
+
catch (error) {
|
|
156
|
+
console.error(`Error loading JSON rules from ${jsonPath}:`, error);
|
|
157
|
+
return [];
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
// JSON Rule Loader — Load rules filtered by pack IDs
|
|
161
|
+
function loadRulesFromJSONByPack(jsonPath, packIds) {
|
|
162
|
+
try {
|
|
163
|
+
const jsonContent = fs.readFileSync(jsonPath, 'utf-8');
|
|
164
|
+
const config = JSON.parse(jsonContent);
|
|
165
|
+
if (!config?.rules || !Array.isArray(config.rules)) {
|
|
166
|
+
console.warn(`No rules found in JSON file: ${jsonPath}`);
|
|
167
|
+
return [];
|
|
168
|
+
}
|
|
169
|
+
const packSet = new Set(packIds);
|
|
170
|
+
const filtered = config.rules.filter(r => r.packs.some(p => packSet.has(p)));
|
|
171
|
+
return filtered.map(jsonRuleToRule);
|
|
172
|
+
}
|
|
173
|
+
catch (error) {
|
|
174
|
+
console.error(`Error loading JSON rules from ${jsonPath}:`, error);
|
|
175
|
+
return [];
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
// Compile raw JSON rule objects (from API/cache) to engine Rule objects
|
|
179
|
+
function compileRawRules(rawRules) {
|
|
180
|
+
return rawRules.map(jsonRuleToRule);
|
|
181
|
+
}
|
|
109
182
|
// Tree-sitter Parser for AST analysis
|
|
110
183
|
class TreeSitterParser {
|
|
111
184
|
constructor() {
|
|
@@ -263,15 +336,37 @@ exports.COPPA_RULES = [
|
|
|
263
336
|
severity: 'critical',
|
|
264
337
|
description: 'Social login (Google, Facebook, Twitter) without age gating is prohibited for child-directed apps',
|
|
265
338
|
patterns: [
|
|
339
|
+
// JS/TS — Firebase
|
|
266
340
|
/signInWithPopup\s*\(\s*\w+\s*,\s*['"](google|facebook|twitter|github)['"]/gi,
|
|
267
341
|
/signInWithPopup\s*\(\s*['"](google|facebook|twitter|github)['"]/gi,
|
|
268
342
|
/signInWithPopup\s*\(\s*\w+\s*,\s*\w+\s*\)/gi,
|
|
269
343
|
/firebase\.auth\(\)\s*\.\s*signInWithPopup/gi,
|
|
270
|
-
/
|
|
344
|
+
// JS/TS — Passport.js
|
|
345
|
+
/passport\.authenticate\s*\(\s*['"](google|facebook|twitter)['"]/gi,
|
|
346
|
+
// Python — django-allauth social providers config
|
|
347
|
+
/SOCIALACCOUNT_PROVIDERS\s*=\s*\{[^}]*(?:google|facebook|twitter|github)/gi,
|
|
348
|
+
// Python — python-social-auth backends
|
|
349
|
+
/SOCIAL_AUTH_(?:GOOGLE|FACEBOOK|TWITTER|GITHUB)_(?:KEY|SECRET)/gi,
|
|
350
|
+
// Python — flask-dance blueprints
|
|
351
|
+
/make_(?:google|facebook|twitter|github)_blueprint\s*\(/gi,
|
|
352
|
+
// Python — authlib OAuth registration
|
|
353
|
+
/oauth\.register\s*\(\s*['"](?:google|facebook|twitter|github)['"]/gi,
|
|
354
|
+
// Go — goth social auth providers
|
|
355
|
+
/goth\.UseProviders\s*\(/gi,
|
|
356
|
+
// Java — Spring Security OAuth2 login
|
|
357
|
+
/\.oauth2Login\s*\(\s*\)/gi,
|
|
358
|
+
// Java — Spring OAuth2 client registration
|
|
359
|
+
/ClientRegistration\.withRegistrationId\s*\(\s*['"](?:google|facebook|twitter|github)['"]/gi,
|
|
360
|
+
// Kotlin/Java — Firebase Android
|
|
361
|
+
/Firebase\.auth\.signInWithCredential/gi,
|
|
362
|
+
// Kotlin/Java — Google Sign-In Android
|
|
363
|
+
/GoogleSignIn\.getClient\s*\(/gi,
|
|
364
|
+
// Kotlin/Java — Facebook Login Android SDK
|
|
365
|
+
/LoginManager\.getInstance\s*\(\s*\)\s*\.logIn/gi
|
|
271
366
|
],
|
|
272
367
|
fixSuggestion: 'Wrap the auth call in a conditional check for user.age >= 13 or use signInWithParentEmail() for children',
|
|
273
368
|
penalty: '$51,744 per violation',
|
|
274
|
-
languages: ['typescript', 'javascript', 'python', 'swift']
|
|
369
|
+
languages: ['typescript', 'javascript', 'python', 'go', 'java', 'kotlin', 'swift']
|
|
275
370
|
},
|
|
276
371
|
{
|
|
277
372
|
id: 'coppa-data-002',
|
|
@@ -310,14 +405,30 @@ exports.COPPA_RULES = [
|
|
|
310
405
|
severity: 'high',
|
|
311
406
|
description: 'High-accuracy geolocation without parental consent is prohibited',
|
|
312
407
|
patterns: [
|
|
408
|
+
// JS/TS — browser Geolocation API
|
|
313
409
|
/navigator\.geolocation\.getCurrentPosition/gi,
|
|
314
410
|
/navigator\.geolocation\.watchPosition/gi,
|
|
411
|
+
// Swift — CoreLocation
|
|
315
412
|
/CLLocationManager\.startUpdatingLocation\(\)/gi,
|
|
316
|
-
/locationServices\.requestLocation/gi
|
|
413
|
+
/locationServices\.requestLocation/gi,
|
|
414
|
+
// Java Android — LocationManager
|
|
415
|
+
/LocationManager\s*\.\s*requestLocationUpdates\s*\(/gi,
|
|
416
|
+
// Java/Kotlin Android — Fused Location Provider (Google Play Services)
|
|
417
|
+
/FusedLocationProviderClient|fusedLocationClient\s*\.\s*(?:requestLocationUpdates|getLastLocation|getCurrentLocation)/gi,
|
|
418
|
+
// Java Android — high accuracy priority
|
|
419
|
+
/LocationRequest\.create\s*\(\s*\)\s*\.\s*setPriority\s*\(\s*LocationRequest\.PRIORITY_HIGH_ACCURACY/gi,
|
|
420
|
+
// Kotlin Android — LocationRequest.Builder
|
|
421
|
+
/LocationRequest\.Builder\s*\(\s*Priority\.PRIORITY_HIGH_ACCURACY/gi,
|
|
422
|
+
// Python — geocoder library
|
|
423
|
+
/geocoder\.(?:ip|google|osm|mapquest)\s*\(/gi,
|
|
424
|
+
// Python — geopy geolocators
|
|
425
|
+
/(?:Nominatim|GoogleV3|Bing)\s*\([^)]*\)\s*\.(?:geocode|reverse)/gi,
|
|
426
|
+
// Android manifest — fine location permission
|
|
427
|
+
/android\.permission\.ACCESS_FINE_LOCATION/gi
|
|
317
428
|
],
|
|
318
429
|
fixSuggestion: 'Downgrade accuracy to kCLLocationAccuracyThreeKilometers or require parental consent',
|
|
319
430
|
penalty: '$51,744 per violation',
|
|
320
|
-
languages: ['typescript', 'javascript', 'swift', 'kotlin']
|
|
431
|
+
languages: ['typescript', 'javascript', 'swift', 'kotlin', 'java', 'python', 'xml']
|
|
321
432
|
},
|
|
322
433
|
{
|
|
323
434
|
id: 'coppa-retention-005',
|
|
@@ -325,11 +436,22 @@ exports.COPPA_RULES = [
|
|
|
325
436
|
severity: 'medium',
|
|
326
437
|
description: 'User schemas must have deleted_at, expiration_date, or TTL index for data retention',
|
|
327
438
|
patterns: [
|
|
328
|
-
/
|
|
439
|
+
// JS/TS — Mongoose schemas
|
|
440
|
+
/new\s+Schema\s*\(\s*\{[^{}]*\}/gi,
|
|
441
|
+
// Python — Django models
|
|
442
|
+
/class\s+(?:User|Child|Student|Profile|Account|Member)\w*\s*\(\s*models\.Model\s*\)/gi,
|
|
443
|
+
// Python — SQLAlchemy declarative models
|
|
444
|
+
/class\s+(?:User|Child|Student|Profile|Account|Member)\w*\s*\(\s*(?:Base|db\.Model)\s*\)/gi,
|
|
445
|
+
// Go — GORM model structs with user-related names
|
|
446
|
+
/type\s+(?:User|Child|Student|Profile|Account|Member)\w*\s+struct\s*\{/gi,
|
|
447
|
+
// Java/Kotlin — JPA @Entity on user-related classes
|
|
448
|
+
/@Entity[\s\S]*?class\s+(?:User|Child|Student|Profile|Account|Member)/gi,
|
|
449
|
+
// Kotlin — data class for user models
|
|
450
|
+
/data\s+class\s+(?:User|Child|Student|Profile|Account|Member)\w*\s*\(/gi
|
|
329
451
|
],
|
|
330
452
|
fixSuggestion: 'Add deleted_at column, expiration_date field, or TTL index to database schema',
|
|
331
453
|
penalty: 'Regulatory audit failure',
|
|
332
|
-
languages: ['typescript', 'javascript', 'python', '
|
|
454
|
+
languages: ['typescript', 'javascript', 'python', 'go', 'java', 'kotlin', 'sql']
|
|
333
455
|
},
|
|
334
456
|
// ========== Rules 6-20 (Sprint 2) ==========
|
|
335
457
|
// Rule 6: Unencrypted PII Transmission
|
|
@@ -520,13 +642,24 @@ exports.COPPA_RULES = [
|
|
|
520
642
|
severity: 'low',
|
|
521
643
|
description: 'Cookies or localStorage storing tracking data or PII requires a consent banner',
|
|
522
644
|
patterns: [
|
|
645
|
+
// JS/TS — browser APIs
|
|
523
646
|
/document\.cookie\s*=\s*[^;]*(?:user|email|name|token|session|track|id|uid|analytics)/gi,
|
|
524
647
|
/localStorage\.setItem\s*\(\s*['"][^'"]*(?:user|email|token|session|track|auth|login|id|uid|analytics)[^'"]*['"]/gi,
|
|
525
|
-
/sessionStorage\.setItem\s*\(\s*['"][^'"]*(?:user|email|token|session|track|auth|login|id|uid|analytics)[^'"]*['"]/gi
|
|
648
|
+
/sessionStorage\.setItem\s*\(\s*['"][^'"]*(?:user|email|token|session|track|auth|login|id|uid|analytics)[^'"]*['"]/gi,
|
|
649
|
+
// Python — Flask/Django response.set_cookie()
|
|
650
|
+
/\.set_cookie\s*\(\s*['"][^'"]*(?:user|email|token|session|track|auth|login|uid|analytics)[^'"]*['"]/gi,
|
|
651
|
+
// Go — net/http SetCookie
|
|
652
|
+
/http\.SetCookie\s*\(\s*\w+\s*,\s*&http\.Cookie\s*\{/gi,
|
|
653
|
+
// Java/Kotlin — HttpServletResponse.addCookie
|
|
654
|
+
/\.addCookie\s*\(\s*new\s+Cookie\s*\(/gi,
|
|
655
|
+
// Java/Kotlin — Spring ResponseCookie
|
|
656
|
+
/ResponseCookie\.from\s*\(/gi,
|
|
657
|
+
// Generic — any language setting cookies with PII field names
|
|
658
|
+
/(?:set_cookie|SetCookie|addCookie|add_cookie)\s*\([^)]*(?:user|email|token|session|track|auth|uid|analytics)/gi
|
|
526
659
|
],
|
|
527
660
|
fixSuggestion: 'Add a cookie consent banner component before setting tracking or PII cookies',
|
|
528
661
|
penalty: 'Compliance warning',
|
|
529
|
-
languages: ['typescript', 'javascript']
|
|
662
|
+
languages: ['typescript', 'javascript', 'python', 'go', 'java', 'kotlin']
|
|
530
663
|
},
|
|
531
664
|
// Rule 17: External Links to Non-Child-Safe Sites
|
|
532
665
|
// Fixed Sprint 4: Exclude privacy/TOS links, mailto, and common safe targets
|
|
@@ -550,15 +683,30 @@ exports.COPPA_RULES = [
|
|
|
550
683
|
severity: 'high',
|
|
551
684
|
description: 'Passing email, name, or phone to analytics.identify() exposes PII to third parties',
|
|
552
685
|
patterns: [
|
|
686
|
+
// JS/TS — client-side analytics SDKs
|
|
553
687
|
/analytics\.identify\s*\([^)]*email/gi,
|
|
554
688
|
/mixpanel\.identify.*email/gi,
|
|
555
689
|
/segment\.identify.*email/gi,
|
|
556
690
|
/amplitude\.identify.*email/gi,
|
|
557
|
-
/identify\s*\(\s*\{[^}]*(?:email|name|phone)[^}]*\}/gi
|
|
691
|
+
/identify\s*\(\s*\{[^}]*(?:email|name|phone)[^}]*\}/gi,
|
|
692
|
+
// Python — Segment analytics-python
|
|
693
|
+
/analytics\.identify\s*\(\s*\w+\s*,\s*\{[^}]*(?:email|name|phone)/gi,
|
|
694
|
+
// Python — Mixpanel people_set with PII
|
|
695
|
+
/mp\.people_set\s*\([^)]*(?:email|\$email|name|phone)/gi,
|
|
696
|
+
// Go — Segment analytics-go Identify with PII
|
|
697
|
+
/analytics\.Enqueue\s*\(\s*analytics\.Identify\s*\{[^}]*(?:Email|Name|Phone)/gi,
|
|
698
|
+
// Java/Kotlin — Amplitude setUserId with PII
|
|
699
|
+
/Amplitude\.getInstance\s*\(\s*\)\s*\.setUserId\s*\([^)]*email/gi,
|
|
700
|
+
// Java/Kotlin — Mixpanel identify with email
|
|
701
|
+
/MixpanelAPI\.\w*identify\s*\([^)]*email/gi,
|
|
702
|
+
// Java/Kotlin — Firebase Analytics with PII
|
|
703
|
+
/FirebaseAnalytics\.setUserId\s*\([^)]*(?:email|name)/gi,
|
|
704
|
+
// Generic — setUserId with email across languages
|
|
705
|
+
/(?:setUserId|set_user_id)\s*\([^)]*(?:email|\.name|phone)/gi
|
|
558
706
|
],
|
|
559
707
|
fixSuggestion: 'Hash user ID and omit email/name from analytics payload',
|
|
560
708
|
penalty: '$51,744 per violation',
|
|
561
|
-
languages: ['typescript', 'javascript']
|
|
709
|
+
languages: ['typescript', 'javascript', 'python', 'go', 'java', 'kotlin']
|
|
562
710
|
},
|
|
563
711
|
// Rule 19: School Official Consent Bypass
|
|
564
712
|
// Fixed Sprint 4: Tightened patterns to match actual auth/registration flows only
|
|
@@ -687,6 +835,222 @@ exports.ETHICAL_RULES = [
|
|
|
687
835
|
languages: ['typescript', 'javascript', 'tsx', 'jsx', 'html']
|
|
688
836
|
}
|
|
689
837
|
];
|
|
838
|
+
// AI-Generated Code Audit Rules
|
|
839
|
+
// Catches patterns commonly introduced by AI coding assistants (Copilot, Claude, Cursor, etc.)
|
|
840
|
+
// that create COPPA compliance risks. AI models often reproduce training data patterns
|
|
841
|
+
// including analytics boilerplate, insecure defaults, and placeholder credentials.
|
|
842
|
+
exports.AI_AUDIT_RULES = [
|
|
843
|
+
// AI-AUDIT-001: Placeholder Analytics
|
|
844
|
+
{
|
|
845
|
+
id: 'AI-AUDIT-001',
|
|
846
|
+
name: 'Placeholder Analytics Script',
|
|
847
|
+
severity: 'high',
|
|
848
|
+
description: 'AI-generated code frequently includes placeholder analytics (UA-XXXXX, G-XXXXXX, fbq) copied from training data. These may activate real tracking without child_directed_treatment flags.',
|
|
849
|
+
patterns: [
|
|
850
|
+
/gtag\s*\(\s*['"]config['"]\s*,\s*['"](?:UA-|G-)X{3,}['"]/gi,
|
|
851
|
+
/fbq\s*\(\s*['"]init['"]\s*,\s*['"](?:0{5,}|1{5,}|X{5,}|YOUR_|PIXEL_ID|123456789)['"]/gi,
|
|
852
|
+
/ga\s*\(\s*['"]create['"]\s*,\s*['"]UA-(?:0{5,}|X{5,}|YOUR_)['"]/gi,
|
|
853
|
+
/['"](?:UA|G)-(?:XXXXXXX|0000000|YOUR_ID|REPLACE_ME)['"]/gi,
|
|
854
|
+
/analytics_id\s*[:=]\s*['"](?:placeholder|test|example|TODO|FIXME)['"]/gi
|
|
855
|
+
],
|
|
856
|
+
fixSuggestion: 'Remove placeholder analytics IDs. If analytics are needed, use a COPPA-compliant provider with child_directed_treatment: true.',
|
|
857
|
+
penalty: 'AI-generated compliance risk',
|
|
858
|
+
languages: ['typescript', 'javascript', 'html']
|
|
859
|
+
},
|
|
860
|
+
// AI-AUDIT-002: Hardcoded Secrets
|
|
861
|
+
{
|
|
862
|
+
id: 'AI-AUDIT-002',
|
|
863
|
+
name: 'AI-Generated Hardcoded Secrets',
|
|
864
|
+
severity: 'critical',
|
|
865
|
+
description: 'AI coding assistants frequently generate placeholder API keys, tokens, and secrets inline. These may be committed to version control and exposed.',
|
|
866
|
+
patterns: [
|
|
867
|
+
/(?:api_?key|apiKey|API_KEY)\s*[:=]\s*['"](?:sk-|pk-|ak-|key-)[a-zA-Z0-9]{10,}['"]/gi,
|
|
868
|
+
/(?:secret|SECRET|token|TOKEN)\s*[:=]\s*['"](?!process\.env)[a-zA-Z0-9_-]{20,}['"]/gi,
|
|
869
|
+
/SUPABASE_(?:ANON_KEY|SERVICE_ROLE_KEY)\s*[:=]\s*['"]ey[a-zA-Z0-9_.+-]{30,}['"]/gi,
|
|
870
|
+
/FIREBASE_(?:API_KEY|CONFIG)\s*[:=]\s*['"]AI[a-zA-Z0-9_-]{30,}['"]/gi,
|
|
871
|
+
/(?:password|passwd|pwd)\s*[:=]\s*['"](?!process\.env)[^'"]{8,}['"]\s*(?:,|;|\})/gi
|
|
872
|
+
],
|
|
873
|
+
fixSuggestion: 'Move all secrets to environment variables. Use process.env.API_KEY or a secrets manager. Never hardcode credentials.',
|
|
874
|
+
penalty: 'Security exposure — credentials in source code',
|
|
875
|
+
languages: ['typescript', 'javascript', 'python', 'java']
|
|
876
|
+
},
|
|
877
|
+
// AI-AUDIT-003: Hallucinated URLs
|
|
878
|
+
{
|
|
879
|
+
id: 'AI-AUDIT-003',
|
|
880
|
+
name: 'Hallucinated/Placeholder API URLs',
|
|
881
|
+
severity: 'medium',
|
|
882
|
+
description: 'AI models often generate fake API endpoints (api.example.com, jsonplaceholder, reqres.in) that may be replaced with real endpoints without proper review.',
|
|
883
|
+
patterns: [
|
|
884
|
+
/fetch\s*\(\s*['"]https?:\/\/(?:api\.example\.com|jsonplaceholder\.typicode\.com|reqres\.in|httpbin\.org|mockapi\.io|dummyjson\.com)[^'"]*['"]/gi,
|
|
885
|
+
/axios\.\w+\s*\(\s*['"]https?:\/\/(?:api\.example\.com|jsonplaceholder\.typicode\.com|reqres\.in|httpbin\.org|dummyjson\.com)[^'"]*['"]/gi,
|
|
886
|
+
/(?:BASE_URL|API_URL|ENDPOINT)\s*[:=]\s*['"]https?:\/\/(?:api\.example\.com|your-api|my-api|TODO|REPLACE)[^'"]*['"]/gi
|
|
887
|
+
],
|
|
888
|
+
fixSuggestion: 'Replace placeholder URLs with actual endpoints from environment variables. Review all API calls for COPPA data handling compliance.',
|
|
889
|
+
penalty: 'AI-generated placeholder risk',
|
|
890
|
+
languages: ['typescript', 'javascript', 'python']
|
|
891
|
+
},
|
|
892
|
+
// AI-AUDIT-004: Copy-Paste Tracking Boilerplate
|
|
893
|
+
{
|
|
894
|
+
id: 'AI-AUDIT-004',
|
|
895
|
+
name: 'Copy-Paste Tracking Boilerplate',
|
|
896
|
+
severity: 'high',
|
|
897
|
+
description: 'AI assistants reproduce common analytics setup patterns from training data. These often include user identification, event tracking, and session recording without consent flows.',
|
|
898
|
+
patterns: [
|
|
899
|
+
/hotjar\.init\s*\(/gi,
|
|
900
|
+
/Sentry\.init\s*\(\s*\{[^}]*dsn/gi,
|
|
901
|
+
/LogRocket\.init\s*\(/gi,
|
|
902
|
+
/FullStory\.init\s*\(/gi,
|
|
903
|
+
/heap\.load\s*\(/gi,
|
|
904
|
+
/posthog\.init\s*\(/gi,
|
|
905
|
+
/amplitude\.init\s*\(/gi,
|
|
906
|
+
/mixpanel\.init\s*\(/gi,
|
|
907
|
+
/window\.clarity\s*\(/gi
|
|
908
|
+
],
|
|
909
|
+
fixSuggestion: 'Remove session recording and analytics initialization unless COPPA consent is obtained. These tools capture keystrokes, mouse movements, and user behavior — all PII for children.',
|
|
910
|
+
penalty: 'Third-party data collection without consent',
|
|
911
|
+
languages: ['typescript', 'javascript', 'html']
|
|
912
|
+
},
|
|
913
|
+
// AI-AUDIT-005: Insecure Defaults
|
|
914
|
+
{
|
|
915
|
+
id: 'AI-AUDIT-005',
|
|
916
|
+
name: 'AI-Generated Insecure Defaults',
|
|
917
|
+
severity: 'medium',
|
|
918
|
+
description: 'AI models commonly generate code with insecure default configurations: CORS *, disabled SSL verification, permissive CSP, or open CORS origins.',
|
|
919
|
+
patterns: [
|
|
920
|
+
/cors\s*\(\s*\{\s*origin\s*:\s*(?:['"]?\*['"]?|true)/gi,
|
|
921
|
+
/Access-Control-Allow-Origin['"]\s*,\s*['"]\*/gi,
|
|
922
|
+
/rejectUnauthorized\s*:\s*false/gi,
|
|
923
|
+
/NODE_TLS_REJECT_UNAUTHORIZED\s*=\s*['"]?0['"]?/gi,
|
|
924
|
+
/content-security-policy['"]\s*,\s*['"]default-src\s+\*/gi,
|
|
925
|
+
/sameSite\s*:\s*['"]none['"]\s*,?\s*secure\s*:\s*false/gi
|
|
926
|
+
],
|
|
927
|
+
fixSuggestion: 'Replace wildcard CORS with specific allowed origins. Enable SSL verification. Use restrictive CSP. Set secure cookies.',
|
|
928
|
+
penalty: 'Security misconfiguration',
|
|
929
|
+
languages: ['typescript', 'javascript', 'python']
|
|
930
|
+
},
|
|
931
|
+
// AI-AUDIT-006: TODO/FIXME Compliance Gaps
|
|
932
|
+
{
|
|
933
|
+
id: 'AI-AUDIT-006',
|
|
934
|
+
name: 'Unresolved Compliance TODOs',
|
|
935
|
+
severity: 'low',
|
|
936
|
+
description: 'AI-generated code often includes TODO/FIXME comments for compliance-related features (consent, age verification, privacy policy) that may ship unimplemented.',
|
|
937
|
+
patterns: [
|
|
938
|
+
/\/\/\s*(?:TODO|FIXME|HACK|XXX).*(?:consent|age\s*verif|privacy\s*policy|parental|coppa|gdpr|data\s*retention)/gi,
|
|
939
|
+
/\/\/\s*(?:TODO|FIXME).*(?:implement|add|need|require).*(?:auth|login|permission|access\s*control)/gi,
|
|
940
|
+
/#\s*(?:TODO|FIXME).*(?:consent|age\s*verif|privacy|parental|coppa)/gi
|
|
941
|
+
],
|
|
942
|
+
fixSuggestion: 'Resolve all compliance-related TODOs before shipping. Each unresolved TODO is a potential COPPA violation in production.',
|
|
943
|
+
penalty: 'Unimplemented compliance requirement',
|
|
944
|
+
languages: ['typescript', 'javascript', 'python', 'java', 'swift', 'kotlin']
|
|
945
|
+
}
|
|
946
|
+
];
|
|
947
|
+
// Australia Safety by Design (SbD) Rules
|
|
948
|
+
// Based on the eSafety Commissioner's Safety by Design framework (Online Safety Act 2021).
|
|
949
|
+
// Detects code patterns that violate SbD principles: service provider responsibility,
|
|
950
|
+
// user empowerment, and transparency/accountability — especially for platforms serving minors.
|
|
951
|
+
exports.AU_SBD_RULES = [
|
|
952
|
+
// AU-SBD-001: Default Public Profiles
|
|
953
|
+
{
|
|
954
|
+
id: 'AU-SBD-001',
|
|
955
|
+
name: 'Default Public Profile Visibility',
|
|
956
|
+
severity: 'high',
|
|
957
|
+
description: 'User profiles default to public or visible. AU Safety by Design requires privacy-by-default for minors — profiles should be private until explicitly changed by the user or a verified parent.',
|
|
958
|
+
patterns: [
|
|
959
|
+
/(?:visibility|profile_?visibility|is_?public|isPublic)\s*[:=]\s*(?:['"]public['"]|true)/gi,
|
|
960
|
+
/default(?:_?visibility|_?privacy|_?profile)\s*[:=]\s*['"](?:public|open|visible|everyone)['"]/gi,
|
|
961
|
+
/(?:privacy|profilePrivacy)\s*[:=]\s*\{[^}]*default\s*:\s*['"](?:public|open|everyone)['"]/gi,
|
|
962
|
+
/(?:showProfile|profileVisible|publicByDefault|show_profile)\s*[:=]\s*true/gi,
|
|
963
|
+
/(?:searchable|discoverable|findable)\s*[:=]\s*true\b/gi,
|
|
964
|
+
],
|
|
965
|
+
fixSuggestion: 'Set profile visibility to "private" by default. Require explicit user action (or parental consent for children) to make profiles public. AU SbD Principle 1: safety as a fundamental design consideration.',
|
|
966
|
+
penalty: 'Default public exposure of minor profiles',
|
|
967
|
+
languages: ['typescript', 'javascript', 'python', 'java', 'swift', 'kotlin', 'php', 'csharp']
|
|
968
|
+
},
|
|
969
|
+
// AU-SBD-002: Missing Report/Block Mechanism
|
|
970
|
+
{
|
|
971
|
+
id: 'AU-SBD-002',
|
|
972
|
+
name: 'Social Features Without Report/Block',
|
|
973
|
+
severity: 'medium',
|
|
974
|
+
description: 'Social interaction features (comments, posts, messaging) detected without corresponding report or block mechanisms. AU SbD Principle 2 requires users to have tools to protect themselves from harmful interactions.',
|
|
975
|
+
patterns: [
|
|
976
|
+
/(?:addComment|postComment|submitComment|createComment|commentCreate)\s*(?:=|:|\()/gi,
|
|
977
|
+
/(?:sendMessage|createMessage|postMessage|submitMessage|messageCreate)\s*(?:=|:|\()/gi,
|
|
978
|
+
/(?:createPost|submitPost|publishPost|addPost|postCreate)\s*(?:=|:|\()/gi,
|
|
979
|
+
/(?:addReview|submitReview|createReview|postReview|reviewCreate)\s*(?:=|:|\()/gi,
|
|
980
|
+
/(?:shareContent|createShare|submitShare)\s*(?:=|:|\()/gi,
|
|
981
|
+
],
|
|
982
|
+
fixSuggestion: 'Implement report and block mechanisms alongside every social feature. Users must be able to report harmful content and block abusive accounts. AU SbD Principle 2: user empowerment and autonomy.',
|
|
983
|
+
penalty: 'Social features without safety controls',
|
|
984
|
+
languages: ['typescript', 'javascript', 'python', 'java', 'swift', 'kotlin', 'php']
|
|
985
|
+
},
|
|
986
|
+
// AU-SBD-003: Unrestricted Direct Messaging
|
|
987
|
+
{
|
|
988
|
+
id: 'AU-SBD-003',
|
|
989
|
+
name: 'Unrestricted Direct Messaging for Minors',
|
|
990
|
+
severity: 'critical',
|
|
991
|
+
description: 'Direct messaging or chat functionality without safety controls (contact restrictions, message filtering, or parental oversight). The AU Online Safety Act requires platforms to take reasonable steps to prevent child exploitation in private communications.',
|
|
992
|
+
patterns: [
|
|
993
|
+
/(?:directMessage|sendDM|privateMess|createDM|dmChannel|startChat|privateChat|initiateChat)\s*(?:=|:|\()/gi,
|
|
994
|
+
/(?:WebSocket|io\.connect|socket\.emit)\s*\([^)]*(?:chat|message|dm|private)/gi,
|
|
995
|
+
/(?:allowDMs?|enableDMs?|allow_?direct_?message|enable_?private_?message)\s*[:=]\s*true/gi,
|
|
996
|
+
/(?:contactStranger|messageAnyone|openChat|unrestricted_?message)\s*[:=]\s*true/gi,
|
|
997
|
+
],
|
|
998
|
+
fixSuggestion: 'Add safety controls to messaging: restrict contacts to approved friends, implement message filtering, enable parental oversight, and log communications for safety review. AU Online Safety Act 2021 s.45-46.',
|
|
999
|
+
penalty: 'Unrestricted private communication channel',
|
|
1000
|
+
languages: ['typescript', 'javascript', 'python', 'java', 'swift', 'kotlin']
|
|
1001
|
+
},
|
|
1002
|
+
// AU-SBD-004: Algorithmic Feeds Without Safety Guardrails
|
|
1003
|
+
{
|
|
1004
|
+
id: 'AU-SBD-004',
|
|
1005
|
+
name: 'Recommendation Algorithm Without Safety Guardrails',
|
|
1006
|
+
severity: 'high',
|
|
1007
|
+
description: 'Content recommendation or feed algorithms detected without safety filtering, content classification, or age-appropriate guardrails. AU SbD requires platforms to assess and mitigate algorithmic harms, particularly for young users.',
|
|
1008
|
+
patterns: [
|
|
1009
|
+
/(?:recommendContent|getRecommendations|suggestContent|personalizedFeed|forYouFeed|contentFeed)\s*(?:=|:|\()/gi,
|
|
1010
|
+
/(?:algorithm|algo)(?:_?feed|_?rank|_?recommend|_?suggest)\s*(?:=|:|\()/gi,
|
|
1011
|
+
/(?:trending|viral|popular)(?:_?content|_?posts|_?feed|_?items)\s*(?:=|:|\()/gi,
|
|
1012
|
+
/(?:engagement_?score|clickBait|engagement_?rank|watch_?next|autoplay_?next)\s*[:=]/gi,
|
|
1013
|
+
/(?:rabbit_?hole|endless_?feed|infinite_?recommend|auto_?suggest)\s*[:=]\s*true/gi,
|
|
1014
|
+
],
|
|
1015
|
+
fixSuggestion: 'Add age-appropriate content filters to recommendation algorithms. Classify content before serving, implement safety guardrails, and provide transparency on how content is selected. AU SbD Principle 3: transparency and accountability.',
|
|
1016
|
+
penalty: 'Unfiltered algorithmic content delivery',
|
|
1017
|
+
languages: ['typescript', 'javascript', 'python', 'java', 'swift', 'kotlin']
|
|
1018
|
+
},
|
|
1019
|
+
// AU-SBD-005: Missing Digital Wellbeing / Screen Time Controls
|
|
1020
|
+
{
|
|
1021
|
+
id: 'AU-SBD-005',
|
|
1022
|
+
name: 'Engagement Features Without Time Awareness',
|
|
1023
|
+
severity: 'medium',
|
|
1024
|
+
description: 'High-engagement features (autoplay, continuous scrolling, notifications) detected without corresponding digital wellbeing controls (screen time limits, break reminders, usage dashboards). AU SbD encourages platforms to build in digital wellbeing tools.',
|
|
1025
|
+
patterns: [
|
|
1026
|
+
/(?:autoplay|auto_?play)\s*[:=]\s*true/gi,
|
|
1027
|
+
/(?:autoPlay|autoPlayNext|playNext|nextEpisode)\s*[:=]\s*true/gi,
|
|
1028
|
+
/(?:continuous_?play|binge_?mode|marathon_?mode|watch_?party)\s*[:=]\s*true/gi,
|
|
1029
|
+
/(?:push_?notification|sendNotification|scheduleNotif|notif_?trigger).*(?:re_?engage|comeback|miss_?you|inactive)/gi,
|
|
1030
|
+
/(?:daily_?reward|login_?bonus|daily_?streak|come_?back_?reward)\s*(?:=|:|\()/gi,
|
|
1031
|
+
],
|
|
1032
|
+
fixSuggestion: 'Implement digital wellbeing features: screen time dashboards, break reminders after sustained use, and configurable usage limits (especially for accounts under 18). AU SbD: promote healthy technology use.',
|
|
1033
|
+
penalty: 'Engagement-maximizing features without wellbeing controls',
|
|
1034
|
+
languages: ['typescript', 'javascript', 'python', 'java', 'swift', 'kotlin']
|
|
1035
|
+
},
|
|
1036
|
+
// AU-SBD-006: Location Sharing Without Explicit Opt-In
|
|
1037
|
+
{
|
|
1038
|
+
id: 'AU-SBD-006',
|
|
1039
|
+
name: 'Location Data Without Explicit Consent',
|
|
1040
|
+
severity: 'critical',
|
|
1041
|
+
description: 'Location data collection or sharing enabled without explicit, informed opt-in. AU SbD and the Privacy Act 1988 require data minimization, especially for children\'s geolocation data — location should never be collected by default.',
|
|
1042
|
+
patterns: [
|
|
1043
|
+
/(?:shareLocation|share_?location|locationSharing|broadcastLocation)\s*[:=]\s*true/gi,
|
|
1044
|
+
/(?:location_?visible|show_?location|display_?location|location_?public)\s*[:=]\s*true/gi,
|
|
1045
|
+
/(?:trackLocation|location_?tracking|geo_?tracking|locationTracker)\s*[:=]\s*true/gi,
|
|
1046
|
+
/navigator\.geolocation\.(?:watchPosition|getCurrentPosition)\s*\(/gi,
|
|
1047
|
+
/(?:CLLocationManager|LocationManager|FusedLocationProvider).*(?:startUpdating|requestLocation|requestLocationUpdates)/gi,
|
|
1048
|
+
],
|
|
1049
|
+
fixSuggestion: 'Require explicit opt-in for any location data collection. Never enable location sharing by default. For minors, require parental consent before any geolocation access. AU Privacy Act 1988 APP 3.2.',
|
|
1050
|
+
penalty: 'Default location data exposure',
|
|
1051
|
+
languages: ['typescript', 'javascript', 'python', 'java', 'swift', 'kotlin']
|
|
1052
|
+
}
|
|
1053
|
+
];
|
|
690
1054
|
/**
|
|
691
1055
|
* Parse a .haloignore file content
|
|
692
1056
|
*
|
|
@@ -776,26 +1140,75 @@ class HaloEngine {
|
|
|
776
1140
|
constructor(config = {}) {
|
|
777
1141
|
this.config = config;
|
|
778
1142
|
this.treeSitter = new TreeSitterParser();
|
|
779
|
-
//
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
1143
|
+
// Rule loading priority chain:
|
|
1144
|
+
// 1. config.loadedRules — pre-compiled rules from CLI API fetch
|
|
1145
|
+
// 2. config.rulesPath — YAML file (legacy)
|
|
1146
|
+
// 3. config.packs — load from bundled rules.json filtered by pack
|
|
1147
|
+
// 4. Default — hardcoded arrays with legacy boolean flags
|
|
1148
|
+
if (config.loadedRules && config.loadedRules.length > 0) {
|
|
1149
|
+
// Priority 1: Pre-loaded rules (from CLI API fetch or external source)
|
|
1150
|
+
this.rules = config.loadedRules;
|
|
1151
|
+
}
|
|
1152
|
+
else if (config.rulesPath) {
|
|
1153
|
+
// Priority 2: YAML file (legacy)
|
|
1154
|
+
const yamlRules = loadRulesFromYAML(config.rulesPath);
|
|
1155
|
+
this.rules = yamlRules.length > 0 ? yamlRules : exports.COPPA_RULES;
|
|
784
1156
|
}
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
:
|
|
1157
|
+
else if (config.packs) {
|
|
1158
|
+
// Priority 3: Load from bundled rules.json by pack IDs
|
|
1159
|
+
const jsonRules = this.loadBundledRulesByPack(config.packs);
|
|
1160
|
+
this.rules = jsonRules.length > 0 ? jsonRules : exports.COPPA_RULES;
|
|
1161
|
+
}
|
|
1162
|
+
else {
|
|
1163
|
+
// Priority 4: Hardcoded arrays with legacy boolean flags
|
|
1164
|
+
this.rules = config.rules
|
|
789
1165
|
? exports.COPPA_RULES.filter(r => config.rules.includes(r.id))
|
|
790
|
-
: exports.COPPA_RULES
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
1166
|
+
: exports.COPPA_RULES;
|
|
1167
|
+
// Append optional packs via legacy boolean flags
|
|
1168
|
+
if (config.ethical) {
|
|
1169
|
+
this.rules = [...this.rules, ...exports.ETHICAL_RULES];
|
|
1170
|
+
}
|
|
1171
|
+
if (config.aiAudit) {
|
|
1172
|
+
this.rules = [...this.rules, ...exports.AI_AUDIT_RULES];
|
|
1173
|
+
}
|
|
1174
|
+
if (config.sectorAuSbd) {
|
|
1175
|
+
this.rules = [...this.rules, ...exports.AU_SBD_RULES];
|
|
1176
|
+
}
|
|
794
1177
|
}
|
|
795
1178
|
if (config.severityFilter) {
|
|
796
1179
|
this.rules = this.rules.filter(r => config.severityFilter.includes(r.severity));
|
|
797
1180
|
}
|
|
798
1181
|
}
|
|
1182
|
+
/**
|
|
1183
|
+
* Load rules from the bundled rules.json file, filtered by pack IDs.
|
|
1184
|
+
* Falls back to empty array if rules.json not found.
|
|
1185
|
+
*/
|
|
1186
|
+
loadBundledRulesByPack(packIds) {
|
|
1187
|
+
try {
|
|
1188
|
+
const jsonPath = path.resolve(__dirname, '..', 'rules', 'rules.json');
|
|
1189
|
+
return loadRulesFromJSONByPack(jsonPath, packIds);
|
|
1190
|
+
}
|
|
1191
|
+
catch (error) {
|
|
1192
|
+
return [];
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
1195
|
+
/**
|
|
1196
|
+
* Resolve legacy boolean flags to pack IDs.
|
|
1197
|
+
* Maps ethical/aiAudit/sectorAuSbd booleans to their pack ID equivalents.
|
|
1198
|
+
* Always includes 'coppa' as the base pack.
|
|
1199
|
+
*/
|
|
1200
|
+
static resolvePacks(config) {
|
|
1201
|
+
if (config.packs)
|
|
1202
|
+
return config.packs;
|
|
1203
|
+
const packs = ['coppa'];
|
|
1204
|
+
if (config.ethical)
|
|
1205
|
+
packs.push('ethical');
|
|
1206
|
+
if (config.aiAudit)
|
|
1207
|
+
packs.push('ai-audit');
|
|
1208
|
+
if (config.sectorAuSbd)
|
|
1209
|
+
packs.push('au-sbd');
|
|
1210
|
+
return packs;
|
|
1211
|
+
}
|
|
799
1212
|
/**
|
|
800
1213
|
* Get the tree-sitter parser for advanced AST analysis
|
|
801
1214
|
*/
|
|
@@ -916,7 +1329,8 @@ class HaloEngine {
|
|
|
916
1329
|
const trimmedLine = lineContent.trim();
|
|
917
1330
|
if (trimmedLine.startsWith('//') || trimmedLine.startsWith('/*') || trimmedLine.startsWith('*') || trimmedLine.startsWith('<!--') || trimmedLine.startsWith('#')) {
|
|
918
1331
|
// Exception: don't skip halo-ignore comments (those are suppression directives)
|
|
919
|
-
|
|
1332
|
+
// Exception: AI-AUDIT-006 intentionally scans comments for compliance TODOs
|
|
1333
|
+
if (!trimmedLine.includes('halo-ignore') && rule.id !== 'AI-AUDIT-006') {
|
|
920
1334
|
continue;
|
|
921
1335
|
}
|
|
922
1336
|
}
|
|
@@ -989,7 +1403,7 @@ class HaloEngine {
|
|
|
989
1403
|
* Get rule by ID
|
|
990
1404
|
*/
|
|
991
1405
|
getRule(ruleId) {
|
|
992
|
-
return exports.COPPA_RULES.find(r => r.id === ruleId);
|
|
1406
|
+
return this.rules.find(r => r.id === ruleId) || exports.COPPA_RULES.find(r => r.id === ruleId);
|
|
993
1407
|
}
|
|
994
1408
|
/**
|
|
995
1409
|
* Explain a rule (for MCP)
|