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.
@@ -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
@@ -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
  };
@@ -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.1.1",
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
- "prepublishOnly": "npm run build && npm run test && npm run lint",
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
  }