ferret-scan 2.1.1 → 2.2.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/CHANGELOG.md +23 -0
- package/bin/ferret.js +5 -5
- package/dist/analyzers/AstAnalyzer.d.ts +5 -1
- package/dist/analyzers/AstAnalyzer.js +25 -4
- package/dist/features/ignoreComments.js +5 -5
- package/dist/features/policyEnforcement.js +3 -2
- package/dist/remediation/Fixer.js +56 -30
- package/dist/remediation/Quarantine.js +55 -5
- package/dist/rules/ai-specific.js +29 -8
- package/dist/rules/backdoors.js +12 -12
- package/dist/rules/correlationRules.js +6 -6
- package/dist/rules/index.d.ts +1 -0
- package/dist/rules/index.js +10 -1
- package/dist/rules/injection.js +33 -8
- package/dist/rules/patterns/common.d.ts +34 -0
- package/dist/rules/patterns/common.js +48 -0
- package/dist/scanner/PatternMatcher.js +19 -2
- package/dist/types.d.ts +6 -0
- package/dist/utils/baseline.d.ts +15 -2
- package/dist/utils/baseline.js +50 -19
- package/dist/utils/contentCache.d.ts +39 -0
- package/dist/utils/contentCache.js +77 -0
- package/dist/utils/glob.d.ts +50 -0
- package/dist/utils/glob.js +84 -0
- package/dist/utils/pathSecurity.js +1 -0
- package/dist/utils/safeRegex.d.ts +94 -0
- package/dist/utils/safeRegex.js +147 -0
- package/dist/utils/schemas.d.ts +6 -0
- package/dist/utils/schemas.js +13 -0
- package/package.json +13 -4
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Safe regex runtime utilities with bounded runtime and match limits
|
|
3
|
+
*
|
|
4
|
+
* Prevents ReDoS attacks and runaway regex matching in user-controlled patterns.
|
|
5
|
+
*/
|
|
6
|
+
export interface BoundedOptions {
|
|
7
|
+
/** Maximum runtime in milliseconds (default: 1000) */
|
|
8
|
+
maxMs?: number;
|
|
9
|
+
/** Maximum number of matches to collect (default: 500) */
|
|
10
|
+
maxMatches?: number;
|
|
11
|
+
}
|
|
12
|
+
export interface BoundedResult {
|
|
13
|
+
/** Array of captured matches */
|
|
14
|
+
matches: RegExpExecArray[];
|
|
15
|
+
/** Whether runtime was truncated due to time/count limits */
|
|
16
|
+
truncated: boolean;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Compile a pattern string into a RegExp, rejecting obviously dangerous patterns.
|
|
20
|
+
*
|
|
21
|
+
* This function screens for common ReDoS patterns and syntax errors before
|
|
22
|
+
* compilation, returning null for unsafe inputs.
|
|
23
|
+
*
|
|
24
|
+
* @param raw The raw pattern string
|
|
25
|
+
* @param flags Regex flags (default: 'gi')
|
|
26
|
+
* @returns Compiled RegExp or null if pattern is unsafe
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* ```typescript
|
|
30
|
+
* const safe = compileSafePattern('test\\d+'); // OK
|
|
31
|
+
* const unsafe = compileSafePattern('(a+)+b'); // null - ReDoS risk
|
|
32
|
+
* const invalid = compileSafePattern('[unclosed'); // null - syntax error
|
|
33
|
+
* ```
|
|
34
|
+
*/
|
|
35
|
+
export declare function compileSafePattern(raw: string, flags?: string): RegExp | null;
|
|
36
|
+
/**
|
|
37
|
+
* Run a regex against content with bounded runtime and match limits.
|
|
38
|
+
*
|
|
39
|
+
* This function wraps RegExp for each step with timeout and match count protection
|
|
40
|
+
* to prevent runaway regex operations from hanging the application.
|
|
41
|
+
*
|
|
42
|
+
* @param pattern The compiled RegExp to run
|
|
43
|
+
* @param content The content to search
|
|
44
|
+
* @param options Runtime limits
|
|
45
|
+
* @returns Result containing matches and truncation status
|
|
46
|
+
*
|
|
47
|
+
* @example
|
|
48
|
+
* ```typescript
|
|
49
|
+
* const pattern = /test\d+/g;
|
|
50
|
+
* const { matches, truncated } = runBounded(pattern, content, { maxMs: 500 });
|
|
51
|
+
* if (truncated) {
|
|
52
|
+
* console.warn('Regex operation was truncated');
|
|
53
|
+
* }
|
|
54
|
+
* ```
|
|
55
|
+
*/
|
|
56
|
+
export declare function runBounded(pattern: RegExp, content: string, options?: BoundedOptions): BoundedResult;
|
|
57
|
+
/**
|
|
58
|
+
* Safe pattern matching that combines compilation and bounded runtime.
|
|
59
|
+
*
|
|
60
|
+
* This is a convenience wrapper that safely compiles a pattern and runs
|
|
61
|
+
* it with bounds, handling both compilation failures and runtime limits.
|
|
62
|
+
*
|
|
63
|
+
* @param rawPattern The raw pattern string
|
|
64
|
+
* @param content The content to search
|
|
65
|
+
* @param flags Regex flags (default: 'gi')
|
|
66
|
+
* @param options Runtime limits
|
|
67
|
+
* @returns Match result or null if pattern is unsafe
|
|
68
|
+
*
|
|
69
|
+
* @example
|
|
70
|
+
* ```typescript
|
|
71
|
+
* const result = safeMatch('test\\d+', content);
|
|
72
|
+
* if (result === null) {
|
|
73
|
+
* console.warn('Unsafe or invalid pattern');
|
|
74
|
+
* } else if (result.truncated) {
|
|
75
|
+
* console.warn('Pattern operation was bounded');
|
|
76
|
+
* } else {
|
|
77
|
+
* console.log(`Found ${result.matches.length} matches`);
|
|
78
|
+
* }
|
|
79
|
+
* ```
|
|
80
|
+
*/
|
|
81
|
+
export declare function safeMatch(rawPattern: string, content: string, flags?: string, options?: BoundedOptions): BoundedResult | null;
|
|
82
|
+
/**
|
|
83
|
+
* Test if a pattern matches content safely, returning boolean result.
|
|
84
|
+
*
|
|
85
|
+
* This is equivalent to RegExp.test() but with safety checks and bounds.
|
|
86
|
+
* Returns false for unsafe patterns or bounded operations.
|
|
87
|
+
*
|
|
88
|
+
* @param rawPattern The raw pattern string
|
|
89
|
+
* @param content The content to test
|
|
90
|
+
* @param flags Regex flags (default: 'i')
|
|
91
|
+
* @returns True if pattern matches safely, false otherwise
|
|
92
|
+
*/
|
|
93
|
+
export declare function safeTest(rawPattern: string, content: string, flags?: string): boolean;
|
|
94
|
+
//# sourceMappingURL=safeRegex.d.ts.map
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Safe regex runtime utilities with bounded runtime and match limits
|
|
3
|
+
*
|
|
4
|
+
* Prevents ReDoS attacks and runaway regex matching in user-controlled patterns.
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* Compile a pattern string into a RegExp, rejecting obviously dangerous patterns.
|
|
8
|
+
*
|
|
9
|
+
* This function screens for common ReDoS patterns and syntax errors before
|
|
10
|
+
* compilation, returning null for unsafe inputs.
|
|
11
|
+
*
|
|
12
|
+
* @param raw The raw pattern string
|
|
13
|
+
* @param flags Regex flags (default: 'gi')
|
|
14
|
+
* @returns Compiled RegExp or null if pattern is unsafe
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* ```typescript
|
|
18
|
+
* const safe = compileSafePattern('test\\d+'); // OK
|
|
19
|
+
* const unsafe = compileSafePattern('(a+)+b'); // null - ReDoS risk
|
|
20
|
+
* const invalid = compileSafePattern('[unclosed'); // null - syntax error
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
export function compileSafePattern(raw, flags = 'gi') {
|
|
24
|
+
// Screen for obvious ReDoS triggers.
|
|
25
|
+
// We only block patterns where the quantifier structure can cause exponential
|
|
26
|
+
// backtracking — simple multi-alternative strings like (foo|bar|baz) are safe.
|
|
27
|
+
const redosPatterns = [
|
|
28
|
+
/(\?\+)/, // Possessive quantifier abuse: a+?+
|
|
29
|
+
/(\+\+)/, // Double plus: a++
|
|
30
|
+
/(\*\*)/, // Double star: a**
|
|
31
|
+
/(\(.*\+\)\+)/, // Nested quantifiers: (a+)+
|
|
32
|
+
/(\(.*\*\)\*)/, // Nested quantifiers: (a*)*
|
|
33
|
+
/(\(.*\+\)\{)/, // Quantified groups: (a+){2,}
|
|
34
|
+
/(\(.*\|.*\)\+)/, // Alternation inside quantified group: (a|b)+
|
|
35
|
+
/(\(.*\|.*\)\*)/, // Alternation inside quantified group: (a|b)*
|
|
36
|
+
/(\(.*\|.*\)\{)/, // Alternation inside bounded group: (a|b){2,}
|
|
37
|
+
];
|
|
38
|
+
for (const redos of redosPatterns) {
|
|
39
|
+
if (redos.test(raw)) {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
// Attempt compilation
|
|
44
|
+
try {
|
|
45
|
+
return new RegExp(raw, flags);
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
// Invalid syntax
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Run a regex against content with bounded runtime and match limits.
|
|
54
|
+
*
|
|
55
|
+
* This function wraps RegExp for each step with timeout and match count protection
|
|
56
|
+
* to prevent runaway regex operations from hanging the application.
|
|
57
|
+
*
|
|
58
|
+
* @param pattern The compiled RegExp to run
|
|
59
|
+
* @param content The content to search
|
|
60
|
+
* @param options Runtime limits
|
|
61
|
+
* @returns Result containing matches and truncation status
|
|
62
|
+
*
|
|
63
|
+
* @example
|
|
64
|
+
* ```typescript
|
|
65
|
+
* const pattern = /test\d+/g;
|
|
66
|
+
* const { matches, truncated } = runBounded(pattern, content, { maxMs: 500 });
|
|
67
|
+
* if (truncated) {
|
|
68
|
+
* console.warn('Regex operation was truncated');
|
|
69
|
+
* }
|
|
70
|
+
* ```
|
|
71
|
+
*/
|
|
72
|
+
export function runBounded(pattern, content, options = {}) {
|
|
73
|
+
const maxMs = options.maxMs ?? 1000;
|
|
74
|
+
const maxMatches = options.maxMatches ?? 500;
|
|
75
|
+
const deadline = Date.now() + maxMs;
|
|
76
|
+
const matches = [];
|
|
77
|
+
let match;
|
|
78
|
+
while ((match = pattern.exec(content)) !== null) {
|
|
79
|
+
// Check time limit
|
|
80
|
+
if (Date.now() > deadline) {
|
|
81
|
+
return { matches, truncated: true };
|
|
82
|
+
}
|
|
83
|
+
// Check match count limit
|
|
84
|
+
if (matches.length >= maxMatches) {
|
|
85
|
+
return { matches, truncated: true };
|
|
86
|
+
}
|
|
87
|
+
matches.push(match);
|
|
88
|
+
// For non-global patterns, break after first match to avoid infinite loop
|
|
89
|
+
if (!pattern.global) {
|
|
90
|
+
break;
|
|
91
|
+
}
|
|
92
|
+
// Prevent infinite loop on zero-length matches for global patterns
|
|
93
|
+
if (match[0].length === 0) {
|
|
94
|
+
pattern.lastIndex++;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return { matches, truncated: false };
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Safe pattern matching that combines compilation and bounded runtime.
|
|
101
|
+
*
|
|
102
|
+
* This is a convenience wrapper that safely compiles a pattern and runs
|
|
103
|
+
* it with bounds, handling both compilation failures and runtime limits.
|
|
104
|
+
*
|
|
105
|
+
* @param rawPattern The raw pattern string
|
|
106
|
+
* @param content The content to search
|
|
107
|
+
* @param flags Regex flags (default: 'gi')
|
|
108
|
+
* @param options Runtime limits
|
|
109
|
+
* @returns Match result or null if pattern is unsafe
|
|
110
|
+
*
|
|
111
|
+
* @example
|
|
112
|
+
* ```typescript
|
|
113
|
+
* const result = safeMatch('test\\d+', content);
|
|
114
|
+
* if (result === null) {
|
|
115
|
+
* console.warn('Unsafe or invalid pattern');
|
|
116
|
+
* } else if (result.truncated) {
|
|
117
|
+
* console.warn('Pattern operation was bounded');
|
|
118
|
+
* } else {
|
|
119
|
+
* console.log(`Found ${result.matches.length} matches`);
|
|
120
|
+
* }
|
|
121
|
+
* ```
|
|
122
|
+
*/
|
|
123
|
+
export function safeMatch(rawPattern, content, flags = 'gi', options = {}) {
|
|
124
|
+
const pattern = compileSafePattern(rawPattern, flags);
|
|
125
|
+
if (pattern === null) {
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
return runBounded(pattern, content, options);
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Test if a pattern matches content safely, returning boolean result.
|
|
132
|
+
*
|
|
133
|
+
* This is equivalent to RegExp.test() but with safety checks and bounds.
|
|
134
|
+
* Returns false for unsafe patterns or bounded operations.
|
|
135
|
+
*
|
|
136
|
+
* @param rawPattern The raw pattern string
|
|
137
|
+
* @param content The content to test
|
|
138
|
+
* @param flags Regex flags (default: 'i')
|
|
139
|
+
* @returns True if pattern matches safely, false otherwise
|
|
140
|
+
*/
|
|
141
|
+
export function safeTest(rawPattern, content, flags = 'i') {
|
|
142
|
+
// For test, we want to check if there's ANY match, so use non-global flags
|
|
143
|
+
const testFlags = flags.replace(/g/g, ''); // Remove global flag for test behavior
|
|
144
|
+
const result = safeMatch(rawPattern, content, testFlags, { maxMatches: 1 });
|
|
145
|
+
return result !== null && result.matches.length > 0 && !result.truncated;
|
|
146
|
+
}
|
|
147
|
+
//# sourceMappingURL=safeRegex.js.map
|
package/dist/utils/schemas.d.ts
CHANGED
|
@@ -1071,6 +1071,10 @@ export declare function safeParseJSON<T>(content: string, schema: z.ZodType<T>,
|
|
|
1071
1071
|
* Useful when you already have a parsed object
|
|
1072
1072
|
*/
|
|
1073
1073
|
export declare function validateSchema<T>(data: unknown, schema: z.ZodType<T>): ParseResult<T>;
|
|
1074
|
+
/** Validates a comma-separated severity string parsed from the CLI. */
|
|
1075
|
+
export declare const SeverityValueSchema: z.ZodEnum<["CRITICAL", "HIGH", "MEDIUM", "LOW", "INFO"]>;
|
|
1076
|
+
/** Validates a comma-separated category string parsed from the CLI. */
|
|
1077
|
+
export declare const ThreatCategoryValueSchema: z.ZodEnum<["exfiltration", "credentials", "injection", "backdoors", "supply-chain", "permissions", "persistence", "obfuscation", "ai-specific", "advanced-hiding", "behavioral"]>;
|
|
1074
1078
|
declare const _default: {
|
|
1075
1079
|
ThreatIndicatorSchema: z.ZodObject<{
|
|
1076
1080
|
value: z.ZodString;
|
|
@@ -2104,6 +2108,8 @@ declare const _default: {
|
|
|
2104
2108
|
description?: string | undefined;
|
|
2105
2109
|
checksum?: string | undefined;
|
|
2106
2110
|
}>;
|
|
2111
|
+
SeverityValueSchema: z.ZodEnum<["CRITICAL", "HIGH", "MEDIUM", "LOW", "INFO"]>;
|
|
2112
|
+
ThreatCategoryValueSchema: z.ZodEnum<["exfiltration", "credentials", "injection", "backdoors", "supply-chain", "permissions", "persistence", "obfuscation", "ai-specific", "advanced-hiding", "behavioral"]>;
|
|
2107
2113
|
safeParseJSON: typeof safeParseJSON;
|
|
2108
2114
|
validateSchema: typeof validateSchema;
|
|
2109
2115
|
};
|
package/dist/utils/schemas.js
CHANGED
|
@@ -235,6 +235,17 @@ export function validateSchema(data, schema) {
|
|
|
235
235
|
};
|
|
236
236
|
}
|
|
237
237
|
}
|
|
238
|
+
// ============================================
|
|
239
|
+
// CLI-parsed value schemas
|
|
240
|
+
// ============================================
|
|
241
|
+
/** Validates a comma-separated severity string parsed from the CLI. */
|
|
242
|
+
export const SeverityValueSchema = z.enum(['CRITICAL', 'HIGH', 'MEDIUM', 'LOW', 'INFO']);
|
|
243
|
+
/** Validates a comma-separated category string parsed from the CLI. */
|
|
244
|
+
export const ThreatCategoryValueSchema = z.enum([
|
|
245
|
+
'exfiltration', 'credentials', 'injection', 'backdoors',
|
|
246
|
+
'supply-chain', 'permissions', 'persistence', 'obfuscation',
|
|
247
|
+
'ai-specific', 'advanced-hiding', 'behavioral',
|
|
248
|
+
]);
|
|
238
249
|
export default {
|
|
239
250
|
ThreatIndicatorSchema,
|
|
240
251
|
ThreatSourceSchema,
|
|
@@ -244,6 +255,8 @@ export default {
|
|
|
244
255
|
ConfigFileSchema,
|
|
245
256
|
BaselineFindingSchema,
|
|
246
257
|
BaselineSchema,
|
|
258
|
+
SeverityValueSchema,
|
|
259
|
+
ThreatCategoryValueSchema,
|
|
247
260
|
safeParseJSON,
|
|
248
261
|
validateSchema,
|
|
249
262
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ferret-scan",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.2.0",
|
|
4
4
|
"description": "Comprehensive AI Agent Security Platform - scan, monitor, and secure AI CLI configurations with IDE integrations, behavior analysis, and compliance frameworks",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -25,9 +25,15 @@
|
|
|
25
25
|
"lint": "eslint src",
|
|
26
26
|
"lint:fix": "eslint src --fix",
|
|
27
27
|
"prepare": "npm run build",
|
|
28
|
-
"
|
|
28
|
+
"schema:generate": "node scripts/generate-json-schema.mjs",
|
|
29
|
+
"schema:check": "node scripts/generate-json-schema.mjs --check",
|
|
30
|
+
"prepublishOnly": "npm run build && npm run schema:generate && npm run test && npm run lint",
|
|
29
31
|
"scan": "node bin/ferret.js scan",
|
|
30
|
-
"check:resources": "node -e \"console.log('RAM:', Math.round(process.memoryUsage().heapUsed / 1024 / 1024) + 'MB')\""
|
|
32
|
+
"check:resources": "node -e \"console.log('RAM:', Math.round(process.memoryUsage().heapUsed / 1024 / 1024) + 'MB')\"",
|
|
33
|
+
"bench": "npm run build && node scripts/bench.mjs",
|
|
34
|
+
"bench:json": "npm run build && node scripts/bench.mjs --json",
|
|
35
|
+
"bench:compare": "node scripts/bench-compare.mjs",
|
|
36
|
+
"docs:api": "typedoc"
|
|
31
37
|
},
|
|
32
38
|
"keywords": [
|
|
33
39
|
"ai-cli",
|
|
@@ -120,15 +126,18 @@
|
|
|
120
126
|
}
|
|
121
127
|
},
|
|
122
128
|
"devDependencies": {
|
|
129
|
+
"@babel/preset-env": "^7.29.2",
|
|
123
130
|
"@eslint/js": "^9.26.0",
|
|
124
131
|
"@types/jest": "^29.5.11",
|
|
125
132
|
"@types/node": "^20.11.0",
|
|
126
133
|
"@typescript-eslint/eslint-plugin": "^8.54.0",
|
|
127
134
|
"@typescript-eslint/parser": "^8.54.0",
|
|
135
|
+
"babel-jest": "^30.3.0",
|
|
128
136
|
"eslint": "^9.26.0",
|
|
129
137
|
"jest": "^29.7.0",
|
|
130
138
|
"ts-jest": "^29.1.1",
|
|
131
139
|
"typescript": "^5.0.0",
|
|
132
|
-
"typescript-eslint": "^8.54.0"
|
|
140
|
+
"typescript-eslint": "^8.54.0",
|
|
141
|
+
"zod-to-json-schema": "^3.23.0"
|
|
133
142
|
}
|
|
134
143
|
}
|