From: Max Wolf Date: Sun, 11 Jan 2026 17:55:34 +0000 (+0100) Subject: Refactor secrets detection into pattern registry (#18) X-Git-Url: http://git.99rst.org/?a=commitdiff_plain;h=d71269c4a737db0f253f1fb9d19ce2ec2c700b95;p=sgasser-llm-shield.git Refactor secrets detection into pattern registry (#18) * Add PatternDetector and DetectionResult interfaces for secrets detection registry * Move all interfaces to patterns/types.ts and use the existing SecretesDetectionResult interface instead of the new DetectionResult * Move pattern detection utility to new patterns/utils.ts module * Refactor secrets detection using a registry system - Create privateKeysDetector, apiKeysDetector, tokensDetector modules - Refactor detectSecrets() to use the pattern registry - Re-export types from detect.ts for backwards compatibility * Change default secrets_detection action to redaction Hint: The example config still shows `action: block` explicitly, with a comment noting that `redact` is the default action if not specified * Fix README default action references and improve overall structure / formatting - Update all references from 'block (default)' to 'redact (default)' - Fix Bearer token documentation (20+ → 40+ chars) - Reorganize Configuration section with consistent headers - Improve table formatting and section descriptions - Use references to reduce duplications and maintenance overhead * Improve type safety in PatternDetector interface Use SecretEntityType instead of string for enabledTypes Set parameter * Update docs to reflect redact as new default action Reorder actions to show default first --------- Co-authored-by: Stefan Gasser --- diff --git a/README.md b/README.md index d12ff9c..973550e 100644 --- a/README.md +++ b/README.md @@ -72,7 +72,7 @@ docker compose up -d Point your app to `http://localhost:3000/openai/v1` instead of `https://api.openai.com/v1`. -Dashboard: http://localhost:3000/dashboard +Dashboard: [http://localhost:3000/dashboard](http://localhost:3000/dashboard) For multiple languages, configuration options, and more: **[Read the docs →](https://pasteguard.com/docs/quickstart)** diff --git a/config.example.yaml b/config.example.yaml index 96e18c2..6ada82e 100644 --- a/config.example.yaml +++ b/config.example.yaml @@ -96,9 +96,10 @@ secrets_detection: enabled: true # Action to take when secrets are detected: - # block: Block the request with HTTP 400 (default, secure-by-default) - # redact: Replace secrets with placeholders, unmask in response (reversible) + # block: Block the request with HTTP 400 + # redact: Replace secrets with placeholders, unmask in response # route_local: Route to local provider (only works in route mode) + # Default: redact (if not specified) action: block # Secret types to detect diff --git a/docs/concepts/secrets-detection.mdx b/docs/concepts/secrets-detection.mdx index 8206536..0db671d 100644 --- a/docs/concepts/secrets-detection.mdx +++ b/docs/concepts/secrets-detection.mdx @@ -35,28 +35,28 @@ PasteGuard detects secrets before PII detection and can block, redact, or route | Action | Description | |--------|-------------| -| `block` | Return HTTP 400, request never reaches LLM (default) | -| `redact` | Replace secrets with placeholders, restore in response | +| `redact` | Replace secrets with placeholders, restore in response (default) | +| `block` | Return HTTP 400, request never reaches LLM | | `route_local` | Route to local LLM (requires route mode) | -### Block (Default) +### Redact (Default) ```yaml secrets_detection: - enabled: true - action: block + action: redact ``` -Request is rejected with HTTP 400. The secret never reaches the LLM. +Secrets are replaced with placeholders and restored in the response (like PII masking). -### Redact +### Block ```yaml secrets_detection: - action: redact + enabled: true + action: block ``` -Secrets are replaced with placeholders and restored in the response (like PII masking). +Request is rejected with HTTP 400. The secret never reaches the LLM. ### Route to Local diff --git a/docs/configuration/secrets-detection.mdx b/docs/configuration/secrets-detection.mdx index 89a23e7..ac79f11 100644 --- a/docs/configuration/secrets-detection.mdx +++ b/docs/configuration/secrets-detection.mdx @@ -8,7 +8,7 @@ description: Configure secrets detection settings ```yaml secrets_detection: enabled: true - action: block + action: redact entities: - OPENSSH_PRIVATE_KEY - PEM_PRIVATE_KEY @@ -21,7 +21,7 @@ secrets_detection: | Option | Default | Description | |--------|---------|-------------| | `enabled` | `true` | Enable secrets detection | -| `action` | `block` | Action when secrets found | +| `action` | `redact` | Action when secrets found | | `entities` | Private keys | Secret types to detect | | `max_scan_chars` | `200000` | Max characters to scan (0 = unlimited) | | `log_detected_types` | `true` | Log detected types (never logs content) | @@ -31,22 +31,22 @@ secrets_detection: | Action | Description | |--------|-------------| -| `block` | Return HTTP 400, request never reaches LLM (default) | -| `redact` | Replace secrets with placeholders, restore in response | +| `redact` | Replace secrets with placeholders, restore in response (default) | +| `block` | Return HTTP 400, request never reaches LLM | | `route_local` | Route to local LLM (requires route mode) | -### Block (Default) +### Redact (Default) ```yaml secrets_detection: - action: block + action: redact ``` -### Redact +### Block ```yaml secrets_detection: - action: redact + action: block ``` ### Route to Local diff --git a/src/config.ts b/src/config.ts index 426456f..764f27c 100644 --- a/src/config.ts +++ b/src/config.ts @@ -105,7 +105,7 @@ const SecretEntityTypes = [ const SecretsDetectionSchema = z.object({ enabled: z.boolean().default(true), - action: z.enum(["block", "redact", "route_local"]).default("block"), + action: z.enum(["block", "redact", "route_local"]).default("redact"), entities: z.array(z.enum(SecretEntityTypes)).default(["OPENSSH_PRIVATE_KEY", "PEM_PRIVATE_KEY"]), max_scan_chars: z.coerce.number().int().min(0).default(200000), redact_placeholder: z.string().default(""), diff --git a/src/secrets/detect.ts b/src/secrets/detect.ts index 85e8f7f..66c962b 100644 --- a/src/secrets/detect.ts +++ b/src/secrets/detect.ts @@ -1,35 +1,16 @@ import type { SecretsDetectionConfig } from "../config"; import type { ChatCompletionRequest } from "../services/llm-client"; import { extractTextContent } from "../utils/content"; +import { patternDetectors } from "./patterns"; +import type { SecretsDetectionResult, SecretsMatch, SecretsRedaction } from "./patterns/types"; -/** - * All supported secret entity types - */ -export type SecretEntityType = - | "OPENSSH_PRIVATE_KEY" - | "PEM_PRIVATE_KEY" - | "API_KEY_OPENAI" - | "API_KEY_AWS" - | "API_KEY_GITHUB" - | "JWT_TOKEN" - | "BEARER_TOKEN"; - -export interface SecretsMatch { - type: SecretEntityType; - count: number; -} - -export interface SecretsRedaction { - start: number; - end: number; - type: SecretEntityType; -} - -export interface SecretsDetectionResult { - detected: boolean; - matches: SecretsMatch[]; - redactions?: SecretsRedaction[]; -} +// Re-export types from patterns module for backwards compatibility +export type { + SecretEntityType, + SecretsDetectionResult, + SecretsMatch, + SecretsRedaction, +} from "./patterns/types"; /** * Extracts all text content from an OpenAI chat completion request @@ -46,49 +27,13 @@ export function extractTextFromRequest(body: ChatCompletionRequest): string { .join("\n"); } -/** - * Helper to detect secrets matching a pattern and add to matches/redactions - */ -function detectPattern( - textToScan: string, - pattern: RegExp, - entityType: SecretEntityType, - matches: SecretsMatch[], - redactions: SecretsRedaction[], - existingPositions?: Set, -): number { - let count = 0; - for (const match of textToScan.matchAll(pattern)) { - if (match.index !== undefined) { - // Skip if this position was already matched by another pattern - if (existingPositions?.has(match.index)) continue; - - count++; - existingPositions?.add(match.index); - redactions.push({ - start: match.index, - end: match.index + match[0].length, - type: entityType, - }); - } - } - if (count > 0) { - matches.push({ type: entityType, count }); - } - return count; -} - /** * Detects secret material (e.g. private keys, API keys, tokens) in text * - * Scans for: - * - OpenSSH private keys: -----BEGIN OPENSSH PRIVATE KEY----- - * - PEM private keys: RSA, PRIVATE KEY, ENCRYPTED PRIVATE KEY - * - OpenAI API keys: sk-... (48+ chars) - * - AWS access keys: AKIA... (20 chars) - * - GitHub tokens: ghp_, gho_, ghu_, ghs_, ghr_ (40+ chars) - * - JWT tokens: eyJ... (three base64 segments) - * - Bearer tokens: Bearer ... (in Authorization-style contexts) + * Uses the pattern registry to scan for various secret types: + * - Private keys: OpenSSH, PEM (RSA, generic, encrypted) + * - API keys: OpenAI, AWS, GitHub + * - Tokens: JWT, Bearer * * Respects max_scan_chars limit for performance. */ @@ -103,107 +48,31 @@ export function detectSecrets( // Apply max_scan_chars limit const textToScan = config.max_scan_chars > 0 ? text.slice(0, config.max_scan_chars) : text; - const matches: SecretsMatch[] = []; - const redactions: SecretsRedaction[] = []; - // Track which entities to detect based on config - const entitiesToDetect = new Set(config.entities); + const enabledTypes = new Set(config.entities); - // OpenSSH private key pattern - if (entitiesToDetect.has("OPENSSH_PRIVATE_KEY")) { - const opensshPattern = - /-----BEGIN OPENSSH PRIVATE KEY-----[\s\S]*?-----END OPENSSH PRIVATE KEY-----/g; - detectPattern(textToScan, opensshPattern, "OPENSSH_PRIVATE_KEY", matches, redactions); - } - - // PEM private key patterns - if (entitiesToDetect.has("PEM_PRIVATE_KEY")) { - // Track all matched positions to avoid double counting - const matchedPositions = new Set(); - - // RSA PRIVATE KEY - const rsaPattern = /-----BEGIN RSA PRIVATE KEY-----[\s\S]*?-----END RSA PRIVATE KEY-----/g; - detectPattern(textToScan, rsaPattern, "PEM_PRIVATE_KEY", matches, redactions, matchedPositions); - - // Remove PEM_PRIVATE_KEY from matches to accumulate all PEM types together - const pemMatch = matches.find((m) => m.type === "PEM_PRIVATE_KEY"); - if (pemMatch) { - matches.splice(matches.indexOf(pemMatch), 1); - } - let totalPemCount = pemMatch?.count || 0; - - // PRIVATE KEY (generic) - exclude RSA matches - const privateKeyPattern = /-----BEGIN PRIVATE KEY-----[\s\S]*?-----END PRIVATE KEY-----/g; - const tempMatches: SecretsMatch[] = []; - detectPattern( - textToScan, - privateKeyPattern, - "PEM_PRIVATE_KEY", - tempMatches, - redactions, - matchedPositions, - ); - totalPemCount += tempMatches[0]?.count || 0; + // Aggregate results from all pattern detectors + const allMatches: SecretsMatch[] = []; + const allRedactions: SecretsRedaction[] = []; - // ENCRYPTED PRIVATE KEY - const encryptedPattern = - /-----BEGIN ENCRYPTED PRIVATE KEY-----[\s\S]*?-----END ENCRYPTED PRIVATE KEY-----/g; - const tempMatches2: SecretsMatch[] = []; - detectPattern( - textToScan, - encryptedPattern, - "PEM_PRIVATE_KEY", - tempMatches2, - redactions, - matchedPositions, - ); - totalPemCount += tempMatches2[0]?.count || 0; + for (const detector of patternDetectors) { + // Skip detectors that don't handle any enabled types + const hasEnabledPattern = detector.patterns.some((p) => enabledTypes.has(p)); + if (!hasEnabledPattern) continue; - if (totalPemCount > 0) { - matches.push({ type: "PEM_PRIVATE_KEY", count: totalPemCount }); + const result = detector.detect(textToScan, enabledTypes); + allMatches.push(...result.matches); + if (result.redactions) { + allRedactions.push(...result.redactions); } } - // OpenAI API keys: sk-... followed by alphanumeric chars - // Modern format: sk-proj-... or sk-... with 48+ total chars - if (entitiesToDetect.has("API_KEY_OPENAI")) { - // Match sk- followed by optional prefix (proj-, etc.) and alphanumeric/dash/underscore - const openaiPattern = /sk-[a-zA-Z0-9_-]{45,}/g; - detectPattern(textToScan, openaiPattern, "API_KEY_OPENAI", matches, redactions); - } - - // AWS access keys: AKIA followed by 16 uppercase alphanumeric chars - if (entitiesToDetect.has("API_KEY_AWS")) { - const awsPattern = /AKIA[0-9A-Z]{16}/g; - detectPattern(textToScan, awsPattern, "API_KEY_AWS", matches, redactions); - } - - // GitHub tokens: ghp_, gho_, ghu_, ghs_, ghr_ followed by 36+ alphanumeric chars - if (entitiesToDetect.has("API_KEY_GITHUB")) { - const githubPattern = /gh[pousr]_[a-zA-Z0-9]{36,}/g; - detectPattern(textToScan, githubPattern, "API_KEY_GITHUB", matches, redactions); - } - - // JWT tokens: three base64url segments separated by dots - // Header starts with eyJ (base64 for {"...), minimum 20 chars per segment - if (entitiesToDetect.has("JWT_TOKEN")) { - const jwtPattern = /eyJ[a-zA-Z0-9_-]{20,}\.eyJ[a-zA-Z0-9_-]{20,}\.[a-zA-Z0-9_-]{20,}/g; - detectPattern(textToScan, jwtPattern, "JWT_TOKEN", matches, redactions); - } - - // Bearer tokens in Authorization-style contexts - // Matches "Bearer " followed by a token (at least 40 chars to reduce placeholder matches) - if (entitiesToDetect.has("BEARER_TOKEN")) { - const bearerPattern = /Bearer\s+[a-zA-Z0-9._-]{40,}/gi; - detectPattern(textToScan, bearerPattern, "BEARER_TOKEN", matches, redactions); - } - // Sort redactions by start position (descending) for safe replacement - redactions.sort((a, b) => b.start - a.start); + allRedactions.sort((a, b) => b.start - a.start); return { - detected: matches.length > 0, - matches, - redactions: redactions.length > 0 ? redactions : undefined, + detected: allMatches.length > 0, + matches: allMatches, + redactions: allRedactions.length > 0 ? allRedactions : undefined, }; } diff --git a/src/secrets/patterns/api-keys.ts b/src/secrets/patterns/api-keys.ts new file mode 100644 index 0000000..438c1cd --- /dev/null +++ b/src/secrets/patterns/api-keys.ts @@ -0,0 +1,44 @@ +import type { PatternDetector, SecretsMatch, SecretsRedaction } from "./types"; +import { detectPattern } from "./utils"; + +/** + * API keys detector + * + * Detects: + * - API_KEY_OPENAI: OpenAI API keys (sk-...) + * - API_KEY_AWS: AWS Access Keys (AKIA...) + * - API_KEY_GITHUB: GitHub tokens (ghp_, gho_, ghu_, ghs_, ghr_) + */ +export const apiKeysDetector: PatternDetector = { + patterns: ["API_KEY_OPENAI", "API_KEY_AWS", "API_KEY_GITHUB"], + + detect(text: string, enabledTypes: Set) { + const matches: SecretsMatch[] = []; + const redactions: SecretsRedaction[] = []; + + // OpenAI API keys: sk-... followed by alphanumeric chars + // Modern format: sk-proj-... or sk-... with 48+ total chars + if (enabledTypes.has("API_KEY_OPENAI")) { + const openaiPattern = /sk-[a-zA-Z0-9_-]{45,}/g; + detectPattern(text, openaiPattern, "API_KEY_OPENAI", matches, redactions); + } + + // AWS access keys: AKIA followed by 16 uppercase alphanumeric chars + if (enabledTypes.has("API_KEY_AWS")) { + const awsPattern = /AKIA[0-9A-Z]{16}/g; + detectPattern(text, awsPattern, "API_KEY_AWS", matches, redactions); + } + + // GitHub tokens: ghp_, gho_, ghu_, ghs_, ghr_ followed by 36+ alphanumeric chars + if (enabledTypes.has("API_KEY_GITHUB")) { + const githubPattern = /gh[pousr]_[a-zA-Z0-9]{36,}/g; + detectPattern(text, githubPattern, "API_KEY_GITHUB", matches, redactions); + } + + return { + detected: matches.length > 0, + matches, + redactions: redactions.length > 0 ? redactions : undefined, + }; + }, +}; diff --git a/src/secrets/patterns/index.ts b/src/secrets/patterns/index.ts new file mode 100644 index 0000000..957f301 --- /dev/null +++ b/src/secrets/patterns/index.ts @@ -0,0 +1,20 @@ +import { apiKeysDetector } from "./api-keys"; +import { privateKeysDetector } from "./private-keys"; +import { tokensDetector } from "./tokens"; +import type { PatternDetector } from "./types"; + +/** + * Registry of all pattern detectors + * + * Each detector handles one or more secret entity types. + * New detectors can be added here to extend secrets detection. + */ +export const patternDetectors: PatternDetector[] = [ + privateKeysDetector, + apiKeysDetector, + tokensDetector, +]; + +// Re-export types and utilities for convenience +export type { PatternDetector, SecretEntityType, SecretsDetectionResult } from "./types"; +export { detectPattern } from "./utils"; diff --git a/src/secrets/patterns/private-keys.ts b/src/secrets/patterns/private-keys.ts new file mode 100644 index 0000000..2325eea --- /dev/null +++ b/src/secrets/patterns/private-keys.ts @@ -0,0 +1,84 @@ +import type { + PatternDetector, + SecretsDetectionResult, + SecretsMatch, + SecretsRedaction, +} from "./types"; +import { detectPattern } from "./utils"; + +/** + * Private keys detector + * + * Detects: + * - OPENSSH_PRIVATE_KEY: OpenSSH format (-----BEGIN OPENSSH PRIVATE KEY-----) + * - PEM_PRIVATE_KEY: PEM formats (RSA, PRIVATE KEY, ENCRYPTED PRIVATE KEY) + */ +export const privateKeysDetector: PatternDetector = { + patterns: ["OPENSSH_PRIVATE_KEY", "PEM_PRIVATE_KEY"], + + detect(text: string, enabledTypes: Set): SecretsDetectionResult { + const matches: SecretsMatch[] = []; + const redactions: SecretsRedaction[] = []; + + // OpenSSH private key pattern + if (enabledTypes.has("OPENSSH_PRIVATE_KEY")) { + const opensshPattern = + /-----BEGIN OPENSSH PRIVATE KEY-----[\s\S]*?-----END OPENSSH PRIVATE KEY-----/g; + detectPattern(text, opensshPattern, "OPENSSH_PRIVATE_KEY", matches, redactions); + } + + // PEM private key patterns + if (enabledTypes.has("PEM_PRIVATE_KEY")) { + // Track all matched positions to avoid double counting + const matchedPositions = new Set(); + + // RSA PRIVATE KEY + const rsaPattern = /-----BEGIN RSA PRIVATE KEY-----[\s\S]*?-----END RSA PRIVATE KEY-----/g; + detectPattern(text, rsaPattern, "PEM_PRIVATE_KEY", matches, redactions, matchedPositions); + + // Remove PEM_PRIVATE_KEY from matches to accumulate all PEM types together + const pemMatch = matches.find((m) => m.type === "PEM_PRIVATE_KEY"); + if (pemMatch) { + matches.splice(matches.indexOf(pemMatch), 1); + } + let totalPemCount = pemMatch?.count || 0; + + // PRIVATE KEY (generic) - exclude RSA matches + const privateKeyPattern = /-----BEGIN PRIVATE KEY-----[\s\S]*?-----END PRIVATE KEY-----/g; + const tempMatches: SecretsMatch[] = []; + detectPattern( + text, + privateKeyPattern, + "PEM_PRIVATE_KEY", + tempMatches, + redactions, + matchedPositions, + ); + totalPemCount += tempMatches[0]?.count || 0; + + // ENCRYPTED PRIVATE KEY + const encryptedPattern = + /-----BEGIN ENCRYPTED PRIVATE KEY-----[\s\S]*?-----END ENCRYPTED PRIVATE KEY-----/g; + const tempMatches2: SecretsMatch[] = []; + detectPattern( + text, + encryptedPattern, + "PEM_PRIVATE_KEY", + tempMatches2, + redactions, + matchedPositions, + ); + totalPemCount += tempMatches2[0]?.count || 0; + + if (totalPemCount > 0) { + matches.push({ type: "PEM_PRIVATE_KEY", count: totalPemCount }); + } + } + + return { + detected: matches.length > 0, + matches, + redactions: redactions.length > 0 ? redactions : undefined, + }; + }, +}; diff --git a/src/secrets/patterns/tokens.ts b/src/secrets/patterns/tokens.ts new file mode 100644 index 0000000..c5a6c02 --- /dev/null +++ b/src/secrets/patterns/tokens.ts @@ -0,0 +1,38 @@ +import type { PatternDetector, SecretsMatch, SecretsRedaction } from "./types"; +import { detectPattern } from "./utils"; + +/** + * Tokens detector + * + * Detects: + * - JWT_TOKEN: JSON Web Tokens (eyJ...) + * - BEARER_TOKEN: Bearer tokens in Authorization-style contexts + */ +export const tokensDetector: PatternDetector = { + patterns: ["JWT_TOKEN", "BEARER_TOKEN"], + + detect(text: string, enabledTypes: Set) { + const matches: SecretsMatch[] = []; + const redactions: SecretsRedaction[] = []; + + // JWT tokens: three base64url segments separated by dots + // Header starts with eyJ (base64 for {"...), minimum 20 chars per segment + if (enabledTypes.has("JWT_TOKEN")) { + const jwtPattern = /eyJ[a-zA-Z0-9_-]{20,}\.eyJ[a-zA-Z0-9_-]{20,}\.[a-zA-Z0-9_-]{20,}/g; + detectPattern(text, jwtPattern, "JWT_TOKEN", matches, redactions); + } + + // Bearer tokens in Authorization-style contexts + // Matches "Bearer " followed by a token (at least 40 chars to reduce placeholder matches) + if (enabledTypes.has("BEARER_TOKEN")) { + const bearerPattern = /Bearer\s+[a-zA-Z0-9._-]{40,}/gi; + detectPattern(text, bearerPattern, "BEARER_TOKEN", matches, redactions); + } + + return { + detected: matches.length > 0, + matches, + redactions: redactions.length > 0 ? redactions : undefined, + }; + }, +}; diff --git a/src/secrets/patterns/types.ts b/src/secrets/patterns/types.ts new file mode 100644 index 0000000..e8ebc02 --- /dev/null +++ b/src/secrets/patterns/types.ts @@ -0,0 +1,42 @@ +/** + * All supported secret entity types + */ +export type SecretEntityType = + | "OPENSSH_PRIVATE_KEY" + | "PEM_PRIVATE_KEY" + | "API_KEY_OPENAI" + | "API_KEY_AWS" + | "API_KEY_GITHUB" + | "JWT_TOKEN" + | "BEARER_TOKEN"; + +export interface SecretsMatch { + type: SecretEntityType; + count: number; +} + +export interface SecretsRedaction { + start: number; + end: number; + type: SecretEntityType; +} + +export interface SecretsDetectionResult { + detected: boolean; + matches: SecretsMatch[]; + redactions?: SecretsRedaction[]; +} + +/** + * Interface for pattern detector modules + * + * Each detector handles one or more secret entity types and provides + * a detect function that scans text for those patterns. + */ +export interface PatternDetector { + /** Entity types this detector can detect */ + patterns: SecretEntityType[]; + + /** Run detection for enabled entity types */ + detect(text: string, enabledTypes: Set): SecretsDetectionResult; +} diff --git a/src/secrets/patterns/utils.ts b/src/secrets/patterns/utils.ts new file mode 100644 index 0000000..aa1d1f2 --- /dev/null +++ b/src/secrets/patterns/utils.ts @@ -0,0 +1,33 @@ +import type { SecretEntityType, SecretsMatch, SecretsRedaction } from "./types"; + +/** + * Helper to detect secrets matching a pattern and collect matches/redactions + */ +export function detectPattern( + text: string, + pattern: RegExp, + entityType: SecretEntityType, + matches: SecretsMatch[], + redactions: SecretsRedaction[], + existingPositions?: Set, +): number { + let count = 0; + for (const match of text.matchAll(pattern)) { + if (match.index !== undefined) { + // Skip if this position was already matched by another pattern + if (existingPositions?.has(match.index)) continue; + + count++; + existingPositions?.add(match.index); + redactions.push({ + start: match.index, + end: match.index + match[0].length, + type: entityType, + }); + } + } + if (count > 0) { + matches.push({ type: entityType, count }); + } + return count; +}